├── python ├── network_helper_lambda │ ├── conf │ │ ├── __init__.py │ │ ├── aws_network_helper_config.py │ │ └── check_aws_network_config.py │ ├── requirements.txt │ ├── aws_network_helper.py │ └── check_aws_network.py └── slack_listener_lambda │ └── aws_network_slack_listener.py ├── .gitignore ├── tests ├── aws-network-helper-lambda-test.json └── aws-network-slack-listener-lambda-test.json ├── aws_services ├── api_body_mapping_templates │ ├── int_response_application_json.json │ └── int_request_application_x-www-form-urlencoded.json ├── s3_files │ └── aws-network-helper-config.json └── iam_policies │ └── aws-network-helper-policy.json ├── docs ├── AWS Network Helper.docx ├── aws_network_helper.png └── AWS Network Helper Architecture.png └── README.md /python/network_helper_lambda/conf/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/network_helper_lambda/requirements.txt: -------------------------------------------------------------------------------- 1 | netaddr==0.7.18 2 | requests==2.11.1 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime-project 2 | *.sublime-workspace 3 | .DS_Store 4 | notes/** 5 | *.pyc 6 | *.log 7 | -------------------------------------------------------------------------------- /tests/aws-network-helper-lambda-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "response_type":"RETURN", 3 | "text":"help" 4 | } -------------------------------------------------------------------------------- /aws_services/api_body_mapping_templates/int_response_application_json.json: -------------------------------------------------------------------------------- 1 | { 2 | "text":"Processing request..." 3 | } -------------------------------------------------------------------------------- /docs/AWS Network Helper.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngmcfarland/aws-network-helper/HEAD/docs/AWS Network Helper.docx -------------------------------------------------------------------------------- /docs/aws_network_helper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngmcfarland/aws-network-helper/HEAD/docs/aws_network_helper.png -------------------------------------------------------------------------------- /docs/AWS Network Helper Architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngmcfarland/aws-network-helper/HEAD/docs/AWS Network Helper Architecture.png -------------------------------------------------------------------------------- /aws_services/api_body_mapping_templates/int_request_application_x-www-form-urlencoded.json: -------------------------------------------------------------------------------- 1 | { 2 | "headers":"X-Amz-Invocation-Type:Event", 3 | "body":$input.json("$") 4 | } -------------------------------------------------------------------------------- /tests/aws-network-slack-listener-lambda-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "body":"token=&text=help&command=/aws-network&response_url=https://hooks.slack.com/dummy" 3 | } -------------------------------------------------------------------------------- /aws_services/s3_files/aws-network-helper-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "slack_token": "", 3 | "slack_command": "/aws-network", 4 | "kms_region": "", 5 | "kms_key_alias": "", 6 | "sns_arn": "arn:aws:sns:::" 7 | } -------------------------------------------------------------------------------- /python/network_helper_lambda/conf/aws_network_helper_config.py: -------------------------------------------------------------------------------- 1 | help_message="""Hey there! I'm a chat bot that's designed to help you troubleshoot those annoying AWS network problems that are preventing you from connecting to your EC2 or RDS instances. Just by asking me a simple question, or making a simple statement, you can give me enough information to go check the health of your instances as well as your Network ACLs and Security Groups. 2 | Here are some examples: 3 | 4 | USER> I cannot connect to my-awesome-ec2-server from my-other-awesome-ec2-server on port 22 5 | or 6 | USER> Help me connect to my-wonderful-rds-instance from my-awesome-ec2-server on TCP port 5432 7 | or 8 | USER> Why can't I connect to s3 from my-awesome-ec2-server? 9 | or 10 | USER> I can't connect to my-wonderful-rds-instance from my computer. 11 | 12 | In any of your statements, you can specify the port you are trying to connect on, and even the IP protocol you're trying to connect with. However, I'm pretty familiar with the standard ports (and even the ephemeral ports!) for Linux servers, Windows servers, and most database types, so if you don't specify a port, I'll take a stab at it. So let's start troubleshooting!!!""" -------------------------------------------------------------------------------- /aws_services/iam_policies/aws-network-helper-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "s3privs", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "s3:GetObject", 9 | "s3:GetObjectVersion" 10 | ], 11 | "Resource": [ 12 | "" 13 | ] 14 | }, 15 | { 16 | "Sid": "snsprivs", 17 | "Effect": "Allow", 18 | "Action": [ 19 | "sns:ConfirmSubscription", 20 | "sns:GetEndpointAttributes", 21 | "sns:GetSubscriptionAttributes", 22 | "sns:GetTopicAttributes", 23 | "sns:Publish", 24 | "sns:Subscribe" 25 | ], 26 | "Resource": [ 27 | "" 28 | ] 29 | }, 30 | { 31 | "Sid": "kmsprivs", 32 | "Effect": "Allow", 33 | "Action": [ 34 | "kms:Decrypt", 35 | "kms:DescribeKey", 36 | "kms:Encrypt", 37 | "kms:ListAliases", 38 | "kms:ListKeys" 39 | ], 40 | "Resource": [ 41 | "" 42 | ] 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /python/network_helper_lambda/conf/check_aws_network_config.py: -------------------------------------------------------------------------------- 1 | usage = "python check_aws_network.py [ ]" 2 | logging_file = 'check_aws_network.log' 3 | logging_level = 'INFO' 4 | ephemeral_index = {'LINUX':{'from':32768,'to':61000},'WINDOWS':{'from':49152,'to':65535},'UNKNOWN':{'from':1024,'to':65535}} 5 | ec2_general_recommendations = ["Verify that the keypair you are using matches the keypair for your destination.","If you are trying to connect to your destination instance from inside the VPC, make sure you are using the private IP address.","Check the CPU load on your destination instance using CloudWatch to make sure that your instance is truly healthy."] 6 | ec2_troubleshoot_url = "http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/TroubleshootingInstancesConnecting.html" 7 | rds_general_recommendations = ["Check available storage for your RDS instance using the AWS console. If your instance is low on storage, it may impact the ability to connect."] 8 | rds_troubleshoot_url = "http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Troubleshooting.html" 9 | web_general_recommendations = ["If you are trying to connect from your computer, make sure a local firewall isn't blocking your connection.","Double check that you are using the correct SSH key in your connection."] 10 | aws_general_recommendations = ["Verify that your instance's IAM role has the correct privileges to access AWS services."] 11 | aws_troubleshoot_url = "http://docs.aws.amazon.com/IAM/latest/UserGuide/troubleshoot.html" 12 | general_recommendations = {'EC2':{'recommendations':ec2_general_recommendations,'url':ec2_troubleshoot_url},'RDS':{'recommendations':rds_general_recommendations,'url':rds_troubleshoot_url},'WEB':{'recommendations':web_general_recommendations},'AWS':{'recommendations':aws_general_recommendations,'url':aws_troubleshoot_url}} 13 | -------------------------------------------------------------------------------- /python/slack_listener_lambda/aws_network_slack_listener.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import boto3 4 | import base64 5 | import urlparse 6 | 7 | 8 | def lambda_handler(event, context): 9 | params = urlparse.parse_qs(event['body']) 10 | 11 | conf = get_config() 12 | expected_token = decrypt_config_value(conf['slack_token'],conf['kms_region']) 13 | 14 | sns_client = boto3.client('sns') 15 | if params['token'][0] != expected_token: 16 | raise Exception("Invalid request!") 17 | slack_response_url = encrypt_config_value(params['response_url'][0],conf['kms_key_alias'],conf['kms_region']) 18 | event_text = re.sub(r'[^\x00-\x7F]+','', params['text'][0]) 19 | slack_event = {'response_type':'SLACK','command':params['command'][0],'text':event_text,'response_url':slack_response_url} 20 | sns_message = {'default':'I received a message','lambda':json.dumps(slack_event)} 21 | response = sns_client.publish(TopicArn=conf['sns_arn'],Message=json.dumps(sns_message),MessageStructure='json') 22 | if 'MessageId' in response: 23 | return 0 24 | else: 25 | raise Exception("Failed to publish message to SNS") 26 | 27 | 28 | def get_config(): 29 | sts_client = boto3.client('sts') 30 | response = sts_client.get_caller_identity() 31 | s3_bucket = "aws-network-helper-{}".format(response['Account']) 32 | s3 = boto3.resource('s3') 33 | s3_object = s3.Object(s3_bucket,'conf/aws-network-helper-config.json') 34 | contents = json.loads(s3_object.get()['Body'].read()) 35 | return contents 36 | 37 | 38 | def decrypt_config_value(encrypted_value,region): 39 | kms_client = boto3.client('kms',region_name=region) 40 | return kms_client.decrypt(CiphertextBlob=base64.b64decode(encrypted_value))['Plaintext'] 41 | 42 | 43 | def encrypt_config_value(unencrypted_value,key_alias,region): 44 | kms_client = boto3.client('kms',region_name=region) 45 | response = kms_client.encrypt(KeyId='alias/{}'.format(key_alias),Plaintext=unencrypted_value) 46 | return base64.b64encode(response['CiphertextBlob']) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Network Helper 2 | Project for submission to the 2016 AWS Serverless Chatbot Competition 3 | [AWS Network Helper Demo Video](https://youtu.be/KB74FRAYTX4) 4 | 5 | ## Goals & Features 6 | The goal of this project is to provide an AWS network troubleshooting script that runs on a serverless architecture, and can be interacted with via Slack as a chat bot. In simple terms, the goal is to be able to respond to input like: 7 | 8 | * Why can’t I connect to ec2-instance-A from ec2-instance-B? 9 | * Troubleshoot connection between ec2-instance-B and rds-instance-C on port 5432 10 | * I cannot connect to S3 from ec2-instance-A 11 | * Help me connect to ec2-instance-B 12 | 13 | In order to respond to inputs like these, the script must be able to analyze these network elements: 14 | 15 | * Ingress and Egress Security Groups 16 | * Ingress and Egress Network ACLs 17 | * Route Tables 18 | * NAT and Internet Gateways 19 | 20 | Also, since information like instance type, port, and ephemeral ports may or may not be provided, the code must be able to look through metadata for these values, or at least make reasonable assumptions for what the user is most likely trying to accomplish. 21 | 22 | The code is currently able to understand all of the statements above, as well as small variations in the wording. It can troubleshoot network settings for the following types of connections in both directions: 23 | 24 | | Instance A | Instance B | Complexities | 25 | |------------|--------------|----------------------------------------------------------| 26 | | EC2 | EC2 | Supports Windows and Linux | 27 | | EC2 | RDS | All RDS engine types supported | 28 | | EC2 | The Internet | Supports instances behind both Internet and NAT Gateways | 29 | | RDS | The Internet | Supports instances behind both Internet and NAT Gateways | 30 | | EC2 | AWS Services | S3, DynamoDB, KMS, SNS, SQS, etc. | 31 | 32 | Wherever possible, the code should also not limit the scope of this project to only use Slack as the messaging interface. 33 | 34 | 35 | ## Architecture 36 | 37 | AWS Services Used: 38 | * API Gateway 39 | * SNS 40 | * S3 41 | * Lambda 42 | * IAM & KMS 43 | 44 | ![Architecture Diagram](/docs/AWS Network Helper Architecture.png?raw=true "AWS Network Helper Architecture Diagram") 45 | 46 | An SNS topic is used between the Slack listener Lambda and the network helper Lambda so that in the future, different listeners could be deployed that use interfaces other than Slack. Other interfaces could include, but are not limited to: 47 | 48 | * A web app 49 | * A scheduled batch job for auditing network settings 50 | * A command line tool 51 | * A Python module 52 | 53 | S3 is used to provide the user with an externalized configuration file for easier changes. The Slack token, SNS ARN, slash command, and other variables can be changed without re-compiling your Lambda 54 | 55 | KMS is used to decrypt the Slack token stored in the configuration file upon use, and is also used to encrypt/decrypt the response URL as it gets passed through SNS. This is an added layer of security. 56 | 57 | ## Installation 58 | 59 | Please follow the installation instructions in the AWS Network Helper documentation: [AWS Network Helper Documentation](/docs/AWS Network Helper.docx) -------------------------------------------------------------------------------- /python/network_helper_lambda/aws_network_helper.py: -------------------------------------------------------------------------------- 1 | from conf import aws_network_helper_config as conf 2 | import check_aws_network 3 | import requests 4 | import logging 5 | import base64 6 | import boto3 7 | import json 8 | import sys 9 | import re 10 | 11 | logger = logging.getLogger() 12 | logger.setLevel(logging.DEBUG) 13 | 14 | def lambda_handler(event,context): 15 | if 'Records' in event: 16 | message_body = json.loads(event['Records'][0]['Sns']['Message']) 17 | else: 18 | if isinstance(event,str): 19 | message_body = json.loads(event) 20 | elif isinstance(event,dict): 21 | message_body = event 22 | logger.info("Starting Lambda...") 23 | s3_conf = get_config() 24 | if message_body['response_type'].upper() == 'SLACK': 25 | slack_response_url = decrypt_config_value(message_body['response_url'],s3_conf['kms_region']) 26 | if not validate_slack_domain(slack_response_url): 27 | raise Exception("Invalid Slack Response URL!") 28 | if not message_body['command'] == s3_conf['slack_command']: 29 | response = "I'm sorry, I don't recognize the command: {}".format(command) 30 | logger.info("Slack Slash Command: {}".format(message_body['command'])) 31 | if message_body['text'].upper() == 'HELP': 32 | response = conf.help_message 33 | else: 34 | if message_body['response_type'].upper() == 'SLACK': 35 | r = requests.post(slack_response_url,data=json.dumps({'text':'Hold on, let me check some things...'})) 36 | results = match_input(message_body['text']) 37 | logger.debug("Results from match_input: {}".format(results)) 38 | if results['match']: 39 | if results['source'] and results['destination'] and results['port'] and results['ip_protocol']: 40 | response = check_aws_network.troubleshoot(source_name=results['source'],destination_name=results['destination'],port=results['port'],ip_protocol=results['ip_protocol']) 41 | elif results['source'] and results['destination'] and results['port']: 42 | response = check_aws_network.troubleshoot(source_name=results['source'],destination_name=results['destination'],port=results['port']) 43 | else: 44 | response = check_aws_network.troubleshoot(source_name=results['source'],destination_name=results['destination']) 45 | else: 46 | response = "I'm sorry, I don't recognize what you're asking." 47 | if message_body['response_type'].upper() == 'SLACK': 48 | response_body = {'text':response} 49 | logger.info("Sending response: {}".format(response_body)) 50 | r = requests.post(slack_response_url,data=json.dumps(response_body)) 51 | logger.info("Response from Slack response URL post: {}".format(r.text)) 52 | elif message_body['response_type'].upper() == 'RETURN': 53 | logger.info("Sending response: {}".format(response)) 54 | return response 55 | 56 | 57 | def get_config(): 58 | sts_client = boto3.client('sts') 59 | response = sts_client.get_caller_identity() 60 | s3_bucket = "aws-network-helper-{}".format(response['Account']) 61 | s3 = boto3.resource('s3') 62 | s3_object = s3.Object(s3_bucket,'conf/aws-network-helper-config.json') 63 | contents = json.loads(s3_object.get()['Body'].read()) 64 | return contents 65 | 66 | 67 | def decrypt_config_value(encrypted_value,region): 68 | kms_client = boto3.client('kms',region_name=region) 69 | return kms_client.decrypt(CiphertextBlob=base64.b64decode(encrypted_value))['Plaintext'] 70 | 71 | 72 | def validate_slack_domain(response_url): 73 | slack_domain = re.compile(r"^https://hooks.slack.com/.*",re.IGNORECASE) 74 | return True if slack_domain.match(response_url) else False 75 | 76 | 77 | def match_input(user_input): 78 | logger.debug("Matching text: {}".format(user_input)) 79 | match_found = False 80 | match1 = re.compile(r"^(why can I not|why cant i|i (cant|cannot)|help me|i want to) connect to ([a-z0-9\-\_\+\=\.\:\/\@\s]*) from ([a-z0-9\-\_\+\=\.\:\/\@\s]*) on (tcp|udp|icmp)?\s?port (\d+)\??\.?$", re.IGNORECASE) 81 | match2 = re.compile(r"^(why can I not|why cant i|i (cant|cannot)|help me|i want to) connect to ([a-z0-9\-\_\+\=\.\:\/\@\s]*) from ([a-z0-9\-\_\+\=\.\:\/\@\s]*)\??\.?$", re.IGNORECASE) 82 | match3 = re.compile(r"^troubleshoot (the|my)?\s?connection between ([a-z0-9\-\_\+\=\.\:\/\@\s]*) and ([a-z0-9\-\_\+\=\.\:\/\@\s]*) on (tcp|udp|icmp)?\s?port (\d+)\.?$",re.IGNORECASE) 83 | match4 = re.compile(r"^troubleshoot (the|my)?\s?connection between ([a-z0-9\-\_\+\=\.\:\/\@\s]*) and ([a-z0-9\-\_\+\=\.\:\/\@\s]*)\.?$",re.IGNORECASE) 84 | match5 = re.compile(r"^(why can I not|why cant i|i (cant|cannot)|help me|i want to) connect to ([a-z0-9\-\_\+\=\.\:\/\@\s]*) on (tcp|udp|icmp)?\s?port (\d+)\.?\??$",re.IGNORECASE) 85 | match6 = re.compile(r"^(why can I not|why cant i|i (cant|cannot)|help me|i want to) connect to ([a-z0-9\-\_\+\=\.\:\/\@\s]*)\.?\??$",re.IGNORECASE) 86 | if match1.match(user_input): 87 | result = match1.match(user_input) 88 | source_instance = result.group(4) 89 | destination_instance = result.group(3) 90 | ip_protocol = result.group(5) 91 | port = int(result.group(6)) 92 | match_found = True 93 | elif match2.match(user_input): 94 | result = match2.match(user_input) 95 | source_instance = result.group(4) 96 | destination_instance = result.group(3) 97 | ip_protocol = None 98 | port = None 99 | match_found = True 100 | elif match3.match(user_input): 101 | result = match3.match(user_input) 102 | source_instance = result.group(2) 103 | destination_instance = result.group(3) 104 | ip_protocol = result.group(4) 105 | port = int(result.group(5)) 106 | match_found = True 107 | elif match4.match(user_input): 108 | result = match4.match(user_input) 109 | source_instance = result.group(2) 110 | destination_instance = result.group(3) 111 | ip_protocol = None 112 | port = None 113 | match_found = True 114 | elif match5.match(user_input): 115 | result = match5.match(user_input) 116 | source_instance = 'my computer' 117 | destination_instance = result.group(3) 118 | ip_protocol = result.group(4) 119 | port = result.group(5) 120 | match_found = True 121 | elif match6.match(user_input): 122 | result = match6.match(user_input) 123 | source_instance = 'my computer' 124 | destination_instance = result.group(3) 125 | ip_protocol = None 126 | port = None 127 | match_found = True 128 | if match_found: 129 | results = {'match':True,'source':source_instance,'destination':destination_instance,'port':port,'ip_protocol':ip_protocol} 130 | else: 131 | results = {'match':False} 132 | return results -------------------------------------------------------------------------------- /python/network_helper_lambda/check_aws_network.py: -------------------------------------------------------------------------------- 1 | from conf import check_aws_network_config as config 2 | from datetime import datetime 3 | import netaddr 4 | import logging 5 | import boto3 6 | import json 7 | import sys 8 | import re 9 | 10 | 11 | logfile = config.logging_file 12 | logging_level = config.logging_level 13 | numeric_level = getattr(logging, logging_level.upper(), None) 14 | if not isinstance(numeric_level, int): 15 | raise ValueError('Invalid log level: {}'.format(logging_level)) 16 | 17 | logging.basicConfig(filename=logfile, filemode='w', format='%(asctime)s | %(levelname)-8s | %(funcName)-15s | %(message)s', datefmt='%H:%M:%S', level=numeric_level) 18 | 19 | 20 | def troubleshoot(source_name,destination_name,port=None,source_type='UNKNOWN',destination_type='UNKNOWN',ip_protocol='tcp'): 21 | proceed = True 22 | response = [] 23 | looks_good = [] 24 | needs_work = [] 25 | recommendations = [] 26 | error_messages = [] 27 | # 28 | # Get metadata for source and destination instances 29 | logging.info("Starting metadata gathering for {} and {}".format(source_name,destination_name)) 30 | source_metadata = get_instance_metadata(source_name,source_type) 31 | if 'error_type' in source_metadata: 32 | logging.error("Error in obtaining metadata for {}:".format(source_name)) 33 | logging.error("{}: {}".format(source_metadata['error_type'],source_metadata['error_msg'])) 34 | error_messages.append("I am not able to retrieve metadata for \"{}\" because {}".format(source_name,source_metadata['error_msg'])) 35 | proceed = False 36 | else: 37 | logging.info("Obtained metadata for {} successfully".format(source_name)) 38 | logging.info("Instance type for {}: {}".format(source_name,source_metadata['instance_type'])) 39 | destination_metadata = get_instance_metadata(destination_name,destination_type) 40 | if 'error_type' in destination_metadata: 41 | logging.error("Error in obtaining metadata for {}:".format(destination_name)) 42 | logging.error("{}: {}".format(destination_metadata['error_type'],destination_metadata['error_msg'])) 43 | error_messages.append("I am not able to retrieve metadata for \"{}\" because {}".format(destination_name,destination_metadata['error_msg'])) 44 | proceed = False 45 | else: 46 | logging.info("Obtained metadata for {} successfully".format(destination_name)) 47 | logging.info("Instance type for {}: {}".format(destination_name,destination_metadata['instance_type'])) 48 | # 49 | # Check if source and destination are both in the same VPC 50 | if proceed and source_metadata['instance_type'] not in ['WEB','AWS'] and destination_metadata['instance_type'] not in ['WEB','AWS']: 51 | logging.info("Checking if {} and {} are in the same AWS VPC".format(source_name,destination_name)) 52 | if source_metadata['vpc_id'] != destination_metadata['vpc_id']: 53 | logging.error("{} and {} are not in the same VPC. Cross VPC connections are not supported by this script".format(source_name,destination_name)) 54 | error_messages.append("{} and {} are not in the same VPC. I can only troubleshoot connections for instances in the same VPC.".format(source_name,destination_name)) 55 | proceed = False 56 | else: 57 | logging.info("{} and {} are in the same VPC ({})".format(source_name,destination_name,source_metadata['vpc_id'])) 58 | # 59 | # Use default ports if no port passed in 60 | if proceed: 61 | if not port: 62 | logging.info("No port provided. Determining default connection port based on destination") 63 | if destination_metadata['instance_type'] == 'EC2': 64 | if destination_metadata['platform'].lower() == 'linux': 65 | port = 22 66 | elif destination_metadata['platform'].lower() == 'windows': 67 | port = 3389 68 | else: 69 | logging.error("No default port known for EC2 platform {}".format(destination_metadata['platform'])) 70 | error_messages.append("I don't know of a default port for EC2 platform: \"{}\"".format(destination_metadata['platform'])) 71 | proceed = False 72 | elif destination_metadata['instance_type'] == 'RDS': 73 | if 'port' in destination_metadata: 74 | port = destination_metadata['port'] 75 | else: 76 | if re.search(r"oracle",destination_metadata['engine'],re.IGNORECASE): 77 | port = 1521 78 | elif re.search(r"(mysql|aurora|mariadb)",destination_metadata['engine'],re.IGNORECASE): 79 | port = 3306 80 | elif re.search(r"postgres",destination_metadata['engine'],re.IGNORECASE): 81 | port = 5432 82 | elif re.search(r"sqlserver",destination_metadata['engine'],re.IGNORECASE): 83 | port = 1433 84 | else: 85 | logging.error("No default port known for RDS engine {}".format(destination_metadata['engine'])) 86 | error_messages.append("I don't know of a default port for RDS engine: \"{}\"".format(destination_metadata['engine'])) 87 | proceed = False 88 | elif destination_metadata['instance_type'] == 'WEB': 89 | port = 'WEB' 90 | elif destination_metadata['instance_type'] == 'AWS': 91 | port = 443 92 | if port: 93 | logging.info("Using port {} for evaluation".format(port)) 94 | # 95 | # Check instance health for source and destination 96 | if proceed: 97 | logging.info("Checking instance health for {} and {}".format(source_name,destination_name)) 98 | source_health = check_health(source_metadata['instance_type'],source_metadata['status']) 99 | destination_health = check_health(destination_metadata['instance_type'],destination_metadata['status']) 100 | if source_health['healthy'] and destination_health['healthy']: 101 | looks_good.append('instance health checks') 102 | logging.info("Source instance {} is available with a status of '{}'".format(source_name,source_health['status'])) 103 | logging.info("Destination instance {} is available with a status of '{}'".format(destination_name,destination_health['status'])) 104 | else: 105 | needs_work.append('instance health') 106 | if not source_health['healthy']: 107 | logging.info("Source instance {} is unavailable with a status of '{}'".format(source_name,source_health['status'])) 108 | recommendations.append("Check health of {}. Current status: {}".format(source_name,source_health['status'])) 109 | if not destination_health['healthy']: 110 | logging.info("Destination instance {} is unavailable with a status of '{}'".format(destination_name,destination_health['status'])) 111 | recommendations.append("Check health of {}. Current status: {}".format(destination_name,destination_health['status'])) 112 | # 113 | # Check what type of traffic to analyze 114 | if proceed: 115 | if source_metadata['instance_type'] not in ['WEB','AWS'] and destination_metadata['instance_type'] not in ['WEB','AWS']: 116 | # Check if source and destination are both in the same Subnet 117 | logging.info("Checking if {} and {} are in the same VPC subnet".format(source_name,destination_name)) 118 | if source_metadata['subnet_id'] == destination_metadata['subnet_id']: 119 | logging.info("{} and {} are in the same subnet ({}). Network ACLs do not need to be checked".format(source_name,destination_name,source_metadata['subnet_id'])) 120 | else: 121 | # 122 | # Check Network ACL Rules 123 | logging.info("{} and {} are not in the same subnet".format(source_name,destination_name)) 124 | logging.info("Checking network ACLs") 125 | acl_traffic_allowed,recommendations = check_network_acls(source_metadata,destination_metadata,port,ip_protocol,recommendations) 126 | if acl_traffic_allowed: 127 | logging.info("Traffic is allowed through network ACLs on {} port {} between {} and {}".format(ip_protocol.upper(),port,source_name,destination_name)) 128 | looks_good.append('network ACLs') 129 | else: 130 | logging.info("Traffic is not allowed through network ACLs on {} port {} between {} and {}. Recommending:".format(ip_protocol.upper(),port,source_name,destination_name)) 131 | needs_work.append('network ACLs') 132 | for recommendation in recommendations: 133 | logging.info(" - {}".format(recommendation)) 134 | # 135 | # Check Security Group Rules 136 | sg_traffic_allowed,recommendations = check_security_groups(source_metadata,destination_metadata,port,ip_protocol,recommendations) 137 | if sg_traffic_allowed: 138 | logging.info("Traffic is allowed through security groups on {} port {} between {} and {}".format(ip_protocol.upper(),port,source_name,destination_name)) 139 | looks_good.append('security groups') 140 | else: 141 | logging.info("Traffic is not allowed through security groups on {} port {} between {} and {} for the following reasons:".format(ip_protocol.upper(),port,source_name,destination_name)) 142 | needs_work.append('security groups') 143 | for recommendation in recommendations: 144 | logging.info(" - {}".format(recommendation)) 145 | else: 146 | # Troubleshoot web traffic 147 | if source_metadata['instance_type'] in ['WEB','AWS']: 148 | web_traffic_direction = 'IN' 149 | logging.info("Direction of web traffic: {}".format(web_traffic_direction)) 150 | logging.info("Getting metadata for network gateway") 151 | gateway_metadata = get_instance_metadata(destination_metadata['subnet_id'],'GATEWAY') 152 | if 'error_type' in gateway_metadata: 153 | logging.error("Error in obtaining metadata for gateway associated with {}:".format(destination_metadata['subnet_id'])) 154 | logging.error("{}: {}".format(gateway_metadata['error_type'],gateway_metadata['error_msg'])) 155 | error_messages.append("I am not able to retrieve metadata for the gateway associated with subnet \"{}\" because {}".format(destination_metadata['subnet_id'],destination_metadata['error_msg'])) 156 | else: 157 | recommendations,needs_work,looks_good = check_web_traffic(source_metadata,destination_metadata,gateway_metadata,web_traffic_direction,port,ip_protocol,recommendations,needs_work,looks_good) 158 | elif destination_metadata['instance_type'] in ['WEB','AWS']: 159 | web_traffic_direction = 'OUT' 160 | logging.info("Direction of web traffic: {}".format(web_traffic_direction)) 161 | logging.info("Getting metadata for network gateway") 162 | gateway_metadata = get_instance_metadata(source_metadata['subnet_id'],'GATEWAY') 163 | if 'error_type' in gateway_metadata: 164 | logging.error("Error in obtaining metadata for gateway associated with {}:".format(destination_metadata['subnet_id'])) 165 | logging.error("{}: {}".format(gateway_metadata['error_type'],gateway_metadata['error_msg'])) 166 | error_messages.append("I am not able to retrieve metadata for the gateway associated with subnet \"{}\" because {}".format(destination_metadata['subnet_id'],destination_metadata['error_msg'])) 167 | else: 168 | recommendations,needs_work,looks_good = check_web_traffic(source_metadata,destination_metadata,gateway_metadata,web_traffic_direction,port,ip_protocol,recommendations,needs_work,looks_good) 169 | # 170 | # Compile results 171 | if len(error_messages) > 0: 172 | response.append("I'm sorry, {}. If you can help me troubleshoot these issues, I can try taking a look at your network again.".format(format_list(error_messages))) 173 | else: 174 | if len(looks_good) > 0: 175 | response.append("I've checked your {} and everything there looks good.".format(format_list(looks_good))) 176 | if len(needs_work) > 0: 177 | response.append("I have some recommendations about your {}:".format(format_list(needs_work))) 178 | for recommendation in recommendations: 179 | response.append(" - {}".format(recommendation)) 180 | else: 181 | if destination_metadata['instance_type'] != 'UNKNOWN': 182 | response.append("Based on everything I've looked at, you should be able to connect. If you are still having issues, here are some general things to check:") 183 | if destination_metadata['instance_type'] == 'EC2': 184 | for recommendation in config.general_recommendations['EC2']['recommendations']: 185 | response.append(" - {}".format(recommendation)) 186 | elif destination_metadata['instance_type'] == 'RDS': 187 | for recommendation in config.general_recommendations['RDS']['recommendations']: 188 | response.append(" - {}".format(recommendation)) 189 | elif destination_metadata['instance_type'] == 'AWS': 190 | for recommendation in config.general_recommendations['AWS']['recommendations']: 191 | response.append(" - {}".format(recommendation)) 192 | elif destination_metadata['instance_type'] == 'WEB': 193 | for recommendation in config.general_recommendations['WEB']['recommendations']: 194 | response.append(" - {}".format(recommendation)) 195 | else: 196 | response.append("Based on everything I've looked at, you should be able to connect.") 197 | if destination_metadata['instance_type'] == 'EC2': 198 | response.append("Additional Documentation: {}".format(config.general_recommendations['EC2']['url'])) 199 | elif destination_metadata['instance_type'] == 'RDS': 200 | response.append("Additional Documentation: {}".format(config.general_recommendations['RDS']['url'])) 201 | elif destination_metadata['instance_type'] == 'AWS': 202 | response.append("Additional Documentation: {}".format(config.general_recommendations['AWS']['url'])) 203 | else: 204 | if source_metadata['instance_type'] == 'EC2': 205 | response.append("Additional Documentation: {}".format(config.general_recommendations['EC2']['url'])) 206 | elif source_metadata['instance_type'] == 'RDS': 207 | response.append("Additional Documentation: {}".format(config.general_recommendations['RDS']['url'])) 208 | elif source_metadata['instance_type'] == 'AWS': 209 | response.append("Additional Documentation: {}".format(config.general_recommendations['AWS']['url'])) 210 | return "\n".join(response) 211 | 212 | 213 | 214 | def get_ec2_metadata(instance_name): 215 | try: 216 | instance_found = False 217 | client = boto3.client('ec2') 218 | if instance_name[0:2].lower() == 'i-': 219 | matching_ec2s = client.describe_instances(InstanceIds=[instance_name.lower()]) 220 | else: 221 | filters = [{'Name':'tag:Name','Values':[instance_name]}] 222 | matching_ec2s = client.describe_instances(Filters=filters) 223 | if len(matching_ec2s['Reservations']) == 1: 224 | instance_found = True 225 | instance_type = 'EC2' 226 | if len(matching_ec2s['Reservations'][0]['Instances']) > 1: 227 | instance_metadata = {'error_type':'ERROR','error_msg':'I found multiple EC2 instances with the name "{}"'.format(instance_name)} 228 | else: 229 | instance_metadata = matching_ec2s['Reservations'][0]['Instances'][0] 230 | return instance_found,instance_type,instance_metadata 231 | elif len(matching_ec2s['Reservations']) > 1: 232 | instance_found = True 233 | instance_type = 'EC2' 234 | instance_metadata = {'error_type':'ERROR','error_msg':'I found multiple EC2 instances with the name "{}"'.format(instance_name)} 235 | return instance_found,instance_type,instance_metadata 236 | else: 237 | instance_metadata = {'error_type':'ERROR','error_msg':'I didn\'t find any EC2 instances with the name/id: "{}"'.format(instance_name)} 238 | return instance_found,'UNKNOWN',instance_metadata 239 | except: 240 | return instance_found,'UNKNOWN',{'error_type':sys.exc_info()[0],'error_msg':sys.exc_info()[1]} 241 | 242 | 243 | def get_rds_metadata(instance_name): 244 | try: 245 | instance_found = False 246 | client = boto3.client('rds') 247 | matching_rds = client.describe_db_instances(DBInstanceIdentifier=instance_name) 248 | if len(matching_rds['DBInstances']) == 1: 249 | instance_found = True 250 | instance_type = 'RDS' 251 | instance_metadata = matching_rds['DBInstances'][0] 252 | return instance_found,instance_type,instance_metadata 253 | elif len(matching_rds['DBInstances']) > 1: 254 | instance_found = True 255 | instance_type = 'RDS' 256 | instance_metadata = {'error_type':'ERROR','error_msg':'I found multiple RDS instances with the name "{}"'.format(instance_name)} 257 | return instance_found,instance_type,instance_metadata 258 | else: 259 | instance_metadata = {'error_type':'ERROR','error_msg':'I did not find an RDS instance with the name "{}"'.format(instance_name)} 260 | return instance_found,'UNKNOWN',instance_metadata 261 | except: 262 | return instance_found,'UNKNOWN',{'error_type':sys.exc_info()[0],'error_msg':sys.exc_info()[1]} 263 | 264 | 265 | def get_gateway_metadata(subnet_id): 266 | gateway_metadata = None 267 | gateway_found = False 268 | client = boto3.client('ec2') 269 | route_table = client.describe_route_tables(Filters=[{'Name':'association.subnet-id','Values':[subnet_id]}]) 270 | if len(route_table['RouteTables']) == 1: 271 | for route in route_table['RouteTables'][0]['Routes']: 272 | if route['State'] == 'active': 273 | if 'GatewayId' in route: 274 | if route['GatewayId'] != 'local': 275 | gateway_metadata = {'id':route['GatewayId'],'type':'IGW','target':route['DestinationCidrBlock']} 276 | gateway_found = True 277 | break 278 | else: 279 | if 'NatGatewayId' in route: 280 | nat_gateway = client.describe_nat_gateways(NatGatewayIds=[route['NatGatewayId']]) 281 | nat_subnet = client.describe_subnets(SubnetIds=[nat_gateway['NatGateways'][0]['SubnetId']]) 282 | gateway_metadata = {'id':route['NatGatewayId'],'type':'NAT','target':route['DestinationCidrBlock'],'subnet':nat_gateway['NatGateways'][0]['SubnetId'],'subnet_cidr':{'type':'cidr','value':nat_subnet['Subnets'][0]['CidrBlock']}} 283 | gateway_found = True 284 | break 285 | if not gateway_metadata: 286 | gateway_metadata = {'error_type':'GATEWAY_NOT_FOUND','error_msg':'I could not identify a gateway in the route table "{}"'.format(route_table['RouteTables'][0]['RouteTableId'])} 287 | elif len(route_table['RouteTables']) == 0: 288 | gateway_metadata = {'error_type':'NO_ROUTE_TABLE','error_msg':'I could not find a route table for subnet "{}"'.format(subnet_id)} 289 | else: 290 | gateway_metadata = {'error_type':'MULTIPLE_ROUTE_TABLES','error_msg':'I found multiple route tables for subnet "{}"'.format(subnet_id)} 291 | return gateway_found,'GATEWAY',gateway_metadata 292 | 293 | 294 | def get_instance_metadata(instance_name,instance_type='UNKNOWN'): 295 | try: 296 | if instance_type == 'UNKNOWN': 297 | if instance_name.upper() in ['INTERNET','WEB','THE INTERNET','THE WEB','MY COMPUTER']: 298 | instance_found = True 299 | instance_type = 'WEB' 300 | instance_metadata = {} 301 | elif instance_name.upper() in ['S3','KMS','SNS','SQS','DYNAMO','DYNAMODB']: 302 | instance_found = True 303 | instance_type = 'AWS' 304 | instance_metadata = {} 305 | else: 306 | instance_found,instance_type,instance_metadata = get_ec2_metadata(instance_name) 307 | if not instance_found: 308 | instance_found,instance_type,instance_metadata = get_rds_metadata(instance_name) 309 | if not instance_found: 310 | instance_found = False 311 | instance_type = 'UNKNOWN' 312 | instance_metadata = {'error_type':'INSTANCE_MATCH_ERROR','error_msg':'I did not find an EC2 or RDS instance with the name "{}"'.format(instance_name)} 313 | elif instance_type == 'EC2': 314 | instance_found,instance_type,instance_metadata = get_ec2_metadata(instance_name) 315 | elif instance_type == 'RDS': 316 | instance_found,instance_type,instance_metadata = get_rds_metadata(instance_name) 317 | elif instance_type == 'GATEWAY': 318 | instance_found,instance_type,instance_metadata = get_gateway_metadata(instance_name) 319 | else: 320 | instance_found = False 321 | instance_type = 'UNKNOWN' 322 | instance_metadata = {'error_type':'UNKOWN_INSTANCE_TYPE_ERROR','error_msg':'an unknown instance type was provided: "{}"'.format(instance_type)} 323 | if instance_found and 'error_type' not in instance_metadata: 324 | metadata = parse_metadata(instance_name,instance_metadata,instance_type) 325 | else: 326 | metadata = instance_metadata 327 | return metadata 328 | except: 329 | return {'error_type':sys.exc_info()[0],'error_msg':sys.exc_info()[1]} 330 | 331 | 332 | def parse_metadata(instance_name,instance_metadata,instance_type): 333 | try: 334 | metadata = {'instance_name':instance_name,'instance_type':instance_type} 335 | if instance_type == 'RDS': 336 | metadata['engine'] = instance_metadata['Engine'] 337 | metadata['status'] = instance_metadata['DBInstanceStatus'] 338 | metadata['security_group_ids'] = [sg['VpcSecurityGroupId'] for sg in instance_metadata['VpcSecurityGroups']] 339 | rds_az = instance_metadata['AvailabilityZone'] 340 | for subnet in instance_metadata['DBSubnetGroup']['Subnets']: 341 | if subnet['SubnetAvailabilityZone']['Name'] == rds_az: 342 | metadata['subnet_id'] = subnet['SubnetIdentifier'] 343 | break 344 | metadata['vpc_id'] = instance_metadata['DBSubnetGroup']['VpcId'] 345 | metadata['publicly_accessible'] = instance_metadata['PubliclyAccessible'] 346 | metadata['port'] = instance_metadata['Endpoint']['Port'] 347 | ec2 = boto3.resource('ec2') 348 | subnet = ec2.Subnet(metadata['subnet_id']) 349 | metadata['ip_address'] = {'type':'cidr','value':subnet.cidr_block} 350 | metadata['platform'] = 'Unknown' 351 | return metadata 352 | elif instance_type == 'EC2': 353 | metadata['instance_id'] = instance_metadata['InstanceId'] 354 | metadata['status'] = instance_metadata['State']['Code'] 355 | metadata['security_group_ids'] = [sg['GroupId'] for sg in instance_metadata['SecurityGroups']] 356 | metadata['subnet_id'] = instance_metadata['SubnetId'] 357 | metadata['vpc_id'] = instance_metadata['VpcId'] 358 | metadata['ip_address'] = {'type':'ip','value':instance_metadata['NetworkInterfaces'][0]['PrivateIpAddress']} 359 | if 'PublicIpAddress' in instance_metadata: 360 | metadata['public_ip'] = instance_metadata['PublicIpAddress'] 361 | if 'Platform' in instance_metadata: 362 | metadata['platform'] = instance_metadata['Platform'] 363 | else: 364 | metadata['platform'] = 'Linux' 365 | return metadata 366 | elif instance_type in ['WEB','AWS']: 367 | metadata['status'] = 'N/A' 368 | return metadata 369 | elif instance_type == 'GATEWAY': 370 | return instance_metadata 371 | else: 372 | return {'error_type':'ERROR','error_msg':'the instance type was not recognized'} 373 | except: 374 | return {'error_type':sys.exc_info()[0],'error_msg':sys.exc_info()[1]} 375 | 376 | 377 | def check_health(instance_type,instance_status): 378 | if instance_type == 'EC2': 379 | if instance_status == 0: 380 | return {'healthy':False,'status':'Pending'} 381 | elif instance_status == 16: 382 | return {'healthy':True,'status':'Running'} 383 | elif instance_status == 32: 384 | return {'healthy':False,'status':'Shutting-Down'} 385 | elif instance_status == 48: 386 | return {'healthy':False,'status':'Terminated'} 387 | elif instance_status == 64: 388 | return {'healthy':False,'status':'Stopping'} 389 | elif instance_status == 80: 390 | return {'healthy':False,'status':'Stopped'} 391 | else: 392 | return {'healthy':False,'status':'Unknown'} 393 | elif instance_type == 'RDS': 394 | if instance_status.upper() == 'AVAILABLE': 395 | return {'healthy':True,'status':instance_status} 396 | else: 397 | return {'healthy':False,'status':instance_status} 398 | elif instance_type in ['WEB','AWS']: 399 | return {'healthy':True,'status':'N/A'} 400 | else: 401 | return {'healthy':False,'status':'Unknown'} 402 | 403 | 404 | def check_security_groups(source_metadata,destination_metadata,port,ip_protocol,recommendations): 405 | source_sg_names,source_ingress,source_egress = get_inbound_outbound_rules('SG',source_metadata['security_group_ids']) 406 | destination_sg_names,destination_ingress,destination_egress = get_inbound_outbound_rules('SG',destination_metadata['security_group_ids']) 407 | source_egress_allowed = loop_through_rules(object_type='SG',rule_list=source_egress,port=port,target_ip=destination_metadata['ip_address'],target_sgs=destination_metadata['security_group_ids'],ip_protocol=ip_protocol) 408 | destination_ingress_allowed = loop_through_rules(object_type='SG',rule_list=destination_ingress,port=port,target_ip=source_metadata['ip_address'],target_sgs=source_metadata['security_group_ids'],ip_protocol=ip_protocol) 409 | if not source_egress_allowed: 410 | recommendations.append("Allow outbound traffic to {} on one of {}'s security groups ({}) for {} port {}".format(destination_metadata['ip_address']['value'],source_metadata['instance_name'],format_list(source_sg_names,'or'),ip_protocol.upper(),port)) 411 | if not destination_ingress_allowed: 412 | recommendations.append("Allow inbound traffic from {} on one of {}'s security groups ({}) for {} port {}".format(source_metadata['ip_address']['value'],destination_metadata['instance_name'],format_list(destination_sg_names,'or'),ip_protocol.upper(),port)) 413 | if source_egress_allowed and destination_ingress_allowed: 414 | return True,recommendations 415 | else: 416 | return False,recommendations 417 | 418 | 419 | def check_network_acls(source_metadata,destination_metadata,port,ip_protocol,recommendations): 420 | source_ephemeral = get_ephemeral_ports(source_metadata['platform']) 421 | source_acl_names,source_ingress,source_egress = get_inbound_outbound_rules('ACL',source_metadata['subnet_id']) 422 | destination_acl_names,destination_ingress,destination_egress = get_inbound_outbound_rules('ACL',destination_metadata['subnet_id']) 423 | source_egress_allowed = loop_through_rules(object_type='ACL',rule_list=source_egress,port=port,target_ip=destination_metadata['ip_address'],ip_protocol=ip_protocol) 424 | destination_ingress_allowed = loop_through_rules(object_type='ACL',rule_list=destination_ingress,port=port,target_ip=source_metadata['ip_address'],ip_protocol=ip_protocol) 425 | destination_egress_allowed = loop_through_rules(object_type='ACL',rule_list=destination_egress,port=port,target_ip=source_metadata['ip_address'],ephemeral_ports=source_ephemeral,ip_protocol=ip_protocol) 426 | source_ingress_allowed = loop_through_rules(object_type='ACL',rule_list=source_ingress,port=port,target_ip=destination_metadata['ip_address'],ephemeral_ports=source_ephemeral,ip_protocol=ip_protocol) 427 | if not source_egress_allowed: 428 | recommendations.append("Allow outbound traffic to {} on {}'s ACL ({}) for {} port {}".format(destination_metadata['ip_address']['value'],source_metadata['instance_name'],source_acl_names[0],ip_protocol.upper(),port)) 429 | if not destination_ingress_allowed: 430 | recommendations.append("Allow inbound traffic from {} on {}'s ACL ({}) for {} port {}".format(source_metadata['ip_address']['value'],destination_metadata['instance_name'],destination_acl_names[0],ip_protocol.upper(),port)) 431 | if not destination_egress_allowed: 432 | recommendations.append("Allow outbound traffic to {} on {}'s ACL ({}) for {} ephemeral port range: {}-{}".format(source_metadata['ip_address']['value'],destination_metadata['instance_name'],destination_acl_names[0],ip_protocol.upper(),source_ephemeral['from'],source_ephemeral['to'])) 433 | if not source_ingress_allowed: 434 | recommendations.append("Allow inbound traffic from {} on {}'s ACL ({}) for {} ephemeral port range: {}-{}".format(destination_metadata['ip_address']['value'],source_metadata['instance_name'],source_acl_names[0],ip_protocol.upper(),source_ephemeral['from'],source_ephemeral['to'])) 435 | if source_egress_allowed and source_ingress_allowed and destination_egress_allowed and destination_ingress_allowed: 436 | return True,recommendations 437 | else: 438 | return False,recommendations 439 | 440 | 441 | def check_web_traffic(source_metadata,destination_metadata,gateway_metadata,web_traffic_direction,port,ip_protocol,recommendations,needs_work,looks_good): 442 | logging.info("Gateway is of type: {}".format(gateway_metadata['type'])) 443 | if web_traffic_direction == 'IN': 444 | if gateway_metadata['type'] == 'NAT': 445 | logging.info("Gateway is of type: {}".format(gateway_metadata['type'])) 446 | needs_work.append('route tables') 447 | recommendations.append("Your instance is behind a NAT gateway, and is not publicly accessible from the internet. Move your instance to a public route table, or connect to it from a server that is within your VPC.") 448 | logging.info("User is trying to connect to instance through NAT gateway") 449 | elif gateway_metadata['type'] == 'IGW': 450 | logging.info("Gateway is of type: {}".format(gateway_metadata['type'])) 451 | if destination_metadata['instance_type'] == 'EC2': 452 | if 'public_ip' not in destination_metadata: 453 | needs_work.append('EC2 instance') 454 | recommendations.append("Your EC2 instance does not have a public IP address which is required for inbound traffic through your internet gateway.") 455 | logging.info("Target instance does not have a public IP address") 456 | else: 457 | logging.info("Target instance has public IP address") 458 | elif destination_metadata['instance_type'] == 'RDS': 459 | if not destination_metadata['publicly_accessible']: 460 | needs_work.append('RDS instance') 461 | recommendations.append("Your RDS instance is not publicly accessible. Change the settings on your RDS instance or connect from an EC2 instance inside your VPC.") 462 | logging.info("Target RDS is not set to be publicly accessible") 463 | else: 464 | logging.info("Target RDS is publicly accessible") 465 | logging.info("Now checking to see if traffic is allowed through ACLs on port {}".format(port)) 466 | acl_traffic_allowed,recommendations = check_network_acls_for_web(source_metadata,destination_metadata,web_traffic_direction,gateway_metadata,port,ip_protocol,recommendations) 467 | if acl_traffic_allowed: 468 | logging.info("Web traffic is allowed through network ACLs on {} port {} between {} and {}".format(ip_protocol.upper(),port,source_metadata['instance_name'],destination_metadata['instance_name'])) 469 | looks_good.append('network ACLs') 470 | else: 471 | logging.info("Web traffic is not allowed through network ACLs on {} port {} between {} and {}. Recommending:".format(ip_protocol.upper(),port,source_metadata['instance_name'],destination_metadata['instance_name'])) 472 | needs_work.append('network ACLs') 473 | for recommendation in recommendations: 474 | logging.info(" - {}".format(recommendation)) 475 | logging.info("Now checking to see if traffic is allowed through security groups on port {}".format(port)) 476 | sg_traffic_allowed,recommendations = check_security_groups_for_web(source_metadata,destination_metadata,web_traffic_direction,gateway_metadata,port,ip_protocol,recommendations) 477 | if sg_traffic_allowed: 478 | logging.info("Web traffic is allowed through security groups on {} port {} between {} and {}".format(ip_protocol.upper(),port,source_metadata['instance_name'],destination_metadata['instance_name'])) 479 | looks_good.append('security groups') 480 | else: 481 | logging.info("Web traffic is not allowed through security groups on {} port {} between {} and {} for the following reasons:".format(ip_protocol.upper(),port,source_metadata['instance_name'],destination_metadata['instance_name'])) 482 | needs_work.append('security groups') 483 | for recommendation in recommendations: 484 | logging.info(" - {}".format(recommendation)) 485 | elif web_traffic_direction == 'OUT': 486 | if gateway_metadata['type'] == 'IGW': 487 | if source_metadata['instance_type'] == 'EC2': 488 | if 'public_ip' not in source_metadata: 489 | needs_work.append('EC2 instance') 490 | recommendations.append("Your EC2 instance does not have a public IP address which is required for return traffic through your internet gateway.") 491 | logging.info("Target instance does not have a public IP address") 492 | else: 493 | logging.info("Target instance has public IP address") 494 | elif source_metadata['instance_type'] == 'RDS': 495 | if not source_metadata['publicly_accessible']: 496 | needs_work.append('RDS instance') 497 | recommendations.append("Your RDS instance is not publicly accessible which is required for return traffic through your internet gateway.") 498 | logging.info("Target RDS is not set to be publicly accessible") 499 | else: 500 | logging.info("Target RDS is publicly accessible") 501 | if port == 'WEB': 502 | web_ports = [80,443] 503 | acls_look_good = [] 504 | sgs_look_good = [] 505 | for web_port in web_ports: 506 | logging.info("Now checking to see if traffic is allowed through ACLs on port {}".format(web_port)) 507 | acl_traffic_allowed,recommendations = check_network_acls_for_web(source_metadata,destination_metadata,web_traffic_direction,gateway_metadata,web_port,ip_protocol,recommendations) 508 | if acl_traffic_allowed: 509 | logging.info("Web traffic is allowed through network ACLs on {} port {} between {} and {}".format(ip_protocol.upper(),web_port,source_metadata['instance_name'],destination_metadata['instance_name'])) 510 | acls_look_good.append('Y') 511 | else: 512 | logging.info("Web traffic is not allowed through network ACLs on {} port {} between {} and {}. Recommending:".format(ip_protocol.upper(),web_port,source_metadata['instance_name'],destination_metadata['instance_name'])) 513 | acls_look_good.append('N') 514 | for recommendation in recommendations: 515 | logging.info(" - {}".format(recommendation)) 516 | logging.info("Now checking to see if traffic is allowed through security groups on port {}".format(web_port)) 517 | sg_traffic_allowed,recommendations = check_security_groups_for_web(source_metadata,destination_metadata,web_traffic_direction,gateway_metadata,web_port,ip_protocol,recommendations) 518 | if sg_traffic_allowed: 519 | logging.info("Web traffic is allowed through security groups on {} port {} between {} and {}".format(ip_protocol.upper(),web_port,source_metadata['instance_name'],destination_metadata['instance_name'])) 520 | sgs_look_good.append('Y') 521 | else: 522 | logging.info("Web traffic is not allowed through security groups on {} port {} between {} and {} for the following reasons:".format(ip_protocol.upper(),web_port,source_metadata['instance_name'],destination_metadata['instance_name'])) 523 | sgs_look_good.append('N') 524 | for recommendation in recommendations: 525 | logging.info(" - {}".format(recommendation)) 526 | needs_work.append('network ACLs') if 'N' in acls_look_good else looks_good.append('network ACLs') 527 | needs_work.append('security groups') if 'N' in sgs_look_good else looks_good.append('security groups') 528 | else: 529 | logging.info("Now checking to see if traffic is allowed through ACLs on port {}".format(port)) 530 | acl_traffic_allowed,recommendations = check_network_acls_for_web(source_metadata,destination_metadata,web_traffic_direction,gateway_metadata,port,ip_protocol,recommendations) 531 | if acl_traffic_allowed: 532 | logging.info("Web traffic is allowed through network ACLs on {} port {} between {} and {}".format(ip_protocol.upper(),port,source_metadata['instance_name'],destination_metadata['instance_name'])) 533 | looks_good.append('network ACLs') 534 | else: 535 | logging.info("Web traffic is not allowed through network ACLs on {} port {} between {} and {}. Recommending:".format(ip_protocol.upper(),port,source_metadata['instance_name'],destination_metadata['instance_name'])) 536 | needs_work.append('network ACLs') 537 | for recommendation in recommendations: 538 | logging.info(" - {}".format(recommendation)) 539 | logging.info("Now checking to see if traffic is allowed through security groups on port {}".format(port)) 540 | sg_traffic_allowed,recommendations = check_security_groups_for_web(source_metadata,destination_metadata,web_traffic_direction,gateway_metadata,port,ip_protocol,recommendations) 541 | if sg_traffic_allowed: 542 | logging.info("Web traffic is allowed through security groups on {} port {} between {} and {}".format(ip_protocol.upper(),port,source_metadata['instance_name'],destination_metadata['instance_name'])) 543 | looks_good.append('security groups') 544 | else: 545 | logging.info("Web traffic is not allowed through security groups on {} port {} between {} and {} for the following reasons:".format(ip_protocol.upper(),port,source_metadata['instance_name'],destination_metadata['instance_name'])) 546 | needs_work.append('security groups') 547 | for recommendation in recommendations: 548 | logging.info(" - {}".format(recommendation)) 549 | return recommendations,needs_work,looks_good 550 | 551 | 552 | def check_security_groups_for_web(source_metadata,destination_metadata,web_traffic_direction,gateway_metadata,port,ip_protocol,recommendations): 553 | if gateway_metadata['type'] == 'IGW': 554 | if web_traffic_direction == 'IN': 555 | destination_sg_names,destination_ingress,destination_egress = get_inbound_outbound_rules('SG',destination_metadata['security_group_ids']) 556 | destination_ingress_allowed = loop_through_rules(object_type='SG',rule_list=destination_ingress,port=port,target_ip={'type':'cidr','value':'0.0.0.0/0'},target_sgs=[],ip_protocol=ip_protocol) 557 | if not destination_ingress_allowed: 558 | recommendations.append("Allow inbound traffic from 0.0.0.0/0 or your IP on one of {}'s security groups ({}) for {} port {}".format(destination_metadata['instance_name'],format_list(destination_sg_names,'or'),ip_protocol.upper(),port)) 559 | return False,recommendations 560 | else: 561 | return True,recommendations 562 | elif web_traffic_direction == 'OUT': 563 | source_sg_names,source_ingress,source_egress = get_inbound_outbound_rules('SG',source_metadata['security_group_ids']) 564 | source_egress_allowed = loop_through_rules(object_type='SG',rule_list=source_egress,port=port,target_ip={'type':'cidr','value':'0.0.0.0/0'},target_sgs=[],ip_protocol=ip_protocol) 565 | if not source_egress_allowed: 566 | recommendations.append("Allow outbound traffic to 0.0.0.0/0 on one of {}'s security groups ({}) for {} port {}".format(source_metadata['instance_name'],format_list(source_sg_names,'or'),ip_protocol.upper(),port)) 567 | return False,recommendations 568 | else: 569 | return True,recommendations 570 | elif gateway_metadata['type'] == 'NAT': 571 | if web_traffic_direction == 'IN': 572 | recommendations.append("Inbound traffic from the internet not allowed to instances in private route tables.") 573 | return False,recommendations 574 | elif web_traffic_direction == 'OUT': 575 | source_sg_names,source_ingress,source_egress = get_inbound_outbound_rules('SG',source_metadata['security_group_ids']) 576 | source_egress_allowed = loop_through_rules(object_type='SG',rule_list=source_egress,port=port,target_ip=gateway_metadata['subnet_cidr'],target_sgs=[],ip_protocol=ip_protocol) 577 | if not source_egress_allowed: 578 | recommendations.append("Allow outbound traffic to {} on one of {}'s security groups ({}) for {} port {}".format(gateway_metadata['subnet_cidr'],source_metadata['instance_name'],format_list(source_sg_names,'or'),ip_protocol.upper(),port)) 579 | return False,recommendations 580 | else: 581 | return True,recommendations 582 | 583 | 584 | def check_network_acls_for_web(source_metadata,destination_metadata,web_traffic_direction,gateway_metadata,port,ip_protocol,recommendations): 585 | if gateway_metadata['type'] == 'IGW': 586 | if web_traffic_direction == 'IN': 587 | source_ephemeral = get_ephemeral_ports('UNKNOWN') 588 | destination_acl_names,destination_ingress,destination_egress = get_inbound_outbound_rules('ACL',destination_metadata['subnet_id']) 589 | destination_ingress_allowed = loop_through_rules(object_type='ACL',rule_list=destination_ingress,port=port,target_ip={'type':'cidr','value':'0.0.0.0/0'},ip_protocol=ip_protocol) 590 | destination_egress_allowed = loop_through_rules(object_type='ACL',rule_list=destination_egress,port=port,target_ip={'type':'cidr','value':'0.0.0.0/0'},ephemeral_ports=source_ephemeral,ip_protocol=ip_protocol) 591 | if not destination_ingress_allowed: 592 | recommendations.append("Allow inbound traffic from 0.0.0.0/0 or your IP on {}'s ACL ({}) for {} port {}".format(destination_metadata['instance_name'],destination_acl_names[0],ip_protocol.upper(),port)) 593 | if not destination_egress_allowed: 594 | recommendations.append("Allow outbound traffic to 0.0.0.0/0 or your IP on {}'s ACL ({}) for {} ephemeral port range: {}-{}".format(destination_metadata['instance_name'],destination_acl_names[0],ip_protocol.upper(),source_ephemeral['from'],source_ephemeral['to'])) 595 | if destination_ingress_allowed and destination_egress_allowed: 596 | return True,recommendations 597 | else: 598 | return False,recommendations 599 | elif web_traffic_direction == 'OUT': 600 | source_ephemeral = get_ephemeral_ports(source_metadata['platform']) 601 | source_acl_names,source_ingress,source_egress = get_inbound_outbound_rules('ACL',source_metadata['subnet_id']) 602 | source_egress_allowed = loop_through_rules(object_type='ACL',rule_list=source_egress,port=port,target_ip={'type':'cidr','value':'0.0.0.0/0'},ip_protocol=ip_protocol) 603 | source_ingress_allowed = loop_through_rules(object_type='ACL',rule_list=source_ingress,port=port,target_ip={'type':'cidr','value':'0.0.0.0/0'},ephemeral_ports=source_ephemeral,ip_protocol=ip_protocol) 604 | if not source_egress_allowed: 605 | recommendations.append("Allow outbound traffic to 0.0.0.0/0 on {}'s ACL ({}) for {} port {}".format(source_metadata['instance_name'],source_acl_names[0],ip_protocol.upper(),port)) 606 | if not source_ingress_allowed: 607 | recommendations.append("Allow inbound traffic from 0.0.0.0/0 on {}'s ACL ({}) for {} ephemeral port range: {}-{}".format(source_metadata['instance_name'],source_acl_names[0],ip_protocol.upper(),source_ephemeral['from'],source_ephemeral['to'])) 608 | if source_egress_allowed and source_ingress_allowed: 609 | return True,recommendations 610 | else: 611 | return False,recommendations 612 | elif gateway_metadata['type'] == 'NAT': 613 | if web_traffic_direction == 'IN': 614 | recommendations.append("Inbound traffic from the internet not allowed to instances in private route tables.") 615 | return False,recommendations 616 | elif web_traffic_direction == 'OUT': 617 | source_ephemeral = get_ephemeral_ports(source_metadata['platform']) 618 | source_acl_names,source_ingress,source_egress = get_inbound_outbound_rules('ACL',source_metadata['subnet_id']) 619 | source_egress_allowed = loop_through_rules(object_type='ACL',rule_list=source_egress,port=port,target_ip=gateway_metadata['subnet_cidr'],ip_protocol=ip_protocol) 620 | source_ingress_allowed = loop_through_rules(object_type='ACL',rule_list=source_ingress,port=port,target_ip=gateway_metadata['subnet_cidr'],ephemeral_ports=source_ephemeral,ip_protocol=ip_protocol) 621 | nat_acl_names,nat_ingress,nat_egress = get_inbound_outbound_rules('ACL',gateway_metadata['subnet']) 622 | nat_ingress_allowed = loop_through_rules(object_type='ACL',rule_list=nat_ingress,port=port,target_ip=gateway_metadata['subnet_cidr'],ip_protocol=ip_protocol) 623 | nat_egress_allowed = loop_through_rules(object_type='ACL',rule_list=nat_egress,port=port,target_ip={'type':'cidr','value':'0.0.0.0/0'},ip_protocol=ip_protocol) 624 | nat_ingress_ephemeral_allowed = loop_through_rules(object_type='ACL',rule_list=nat_ingress,port=port,target_ip={'type':'cidr','value':'0.0.0.0/0'},ephemeral_ports=source_ephemeral,ip_protocol=ip_protocol) 625 | nat_egress_ephemeral_allowed = loop_through_rules(object_type='ACL',rule_list=nat_egress,port=port,target_ip=gateway_metadata['subnet_cidr'],ephemeral_ports=source_ephemeral,ip_protocol=ip_protocol) 626 | if not source_egress_allowed: 627 | recommendations.append("Allow outbound traffic to {} on {}'s ACL ({}) for {} port {}".format(gateway_metadata['subnet_cidr'],source_metadata['instance_name'],source_acl_names[0],ip_protocol.upper(),port)) 628 | if not source_ingress_allowed: 629 | recommendations.append("Allow inbound traffic from {} on {}'s ACL ({}) for {} ephemeral port range: {}-{}".format(gateway_metadata['subnet_cidr'],source_metadata['instance_name'],source_acl_names[0],ip_protocol.upper(),source_ephemeral['from'],source_ephemeral['to'])) 630 | if not nat_ingress_allowed: 631 | recommendations.append("Allow inbound traffic from {} on your NAT gateways ACL ({}) for {} port {}".format(source_metadata['ip_address']['value'],nat_acl_names[0],ip_protocol.upper(),port)) 632 | if not nat_egress_allowed: 633 | recommendations.append("Allow outbound traffic to 0.0.0.0/0 on your NAT gateways ACL ({}) for {} port {}".format(nat_acl_names[0],ip_protocol.upper(),port)) 634 | if not nat_ingress_ephemeral_allowed: 635 | recommendations.append("Allow inbound traffic from 0.0.0.0/0 on your NAT gateways ACL ({}) for {} ephemeral port range: {}-{}".format(nat_acl_names[0],ip_protocol.upper(),source_ephemeral['from'],source_ephemeral['to'])) 636 | if not nat_egress_ephemeral_allowed: 637 | recommendations.append("Allow outbound traffic to {} on your NAT gateways ACL ({}) for {} ephemeral port range: {}-{}".format(source_metadata['ip_address']['value'],nat_acl_names[0],ip_protocol.upper(),source_ephemeral['from'],source_ephemeral['to'])) 638 | if source_egress_allowed and source_ingress_allowed and nat_ingress_allowed and nat_egress_allowed and nat_ingress_ephemeral_allowed and nat_egress_ephemeral_allowed: 639 | return True,recommendations 640 | else: 641 | return False,recommendations 642 | 643 | 644 | def get_inbound_outbound_rules(object_type,object_ids): 645 | ec2 = boto3.resource('ec2') 646 | inbound_rules = [] 647 | outbound_rules = [] 648 | if object_type == 'SG': 649 | object_names = [] 650 | for sg_id in object_ids: 651 | sg = ec2.SecurityGroup(sg_id) 652 | object_names.append(sg.group_name) 653 | for inbound_rule in sg.ip_permissions: 654 | metadata = {} 655 | if inbound_rule['IpProtocol'] == '-1': 656 | metadata['port_range'] = {'from':0,'to':65535} 657 | metadata['ip_protocol'] = 'all' 658 | else: 659 | metadata['port_range'] = {'from':inbound_rule['FromPort'],'to':inbound_rule['ToPort']} 660 | metadata['ip_protocol'] = inbound_rule['IpProtocol'] 661 | metadata['grantees'] = [] 662 | if len(inbound_rule['IpRanges']) > 0: 663 | for grantee in inbound_rule['IpRanges']: 664 | metadata['grantees'].append({'type':'cidr','value':grantee['CidrIp']}) 665 | elif len(inbound_rule['UserIdGroupPairs']) > 0: 666 | for grantee in inbound_rule['UserIdGroupPairs']: 667 | metadata['grantees'].append({'type':'sg','value':grantee['GroupId']}) 668 | inbound_rules.append(metadata) 669 | for outbound_rule in sg.ip_permissions_egress: 670 | metadata = {} 671 | if outbound_rule['IpProtocol'] == '-1': 672 | metadata['port_range'] = {'from':0,'to':65535} 673 | metadata['ip_protocol'] = 'all' 674 | else: 675 | metadata['port_range'] = {'from':outbound_rule['FromPort'],'to':outbound_rule['ToPort']} 676 | metadata['ip_protocol'] = outbound_rule['IpProtocol'] 677 | metadata['grantees'] = [] 678 | if len(outbound_rule['IpRanges']) > 0: 679 | for grantee in outbound_rule['IpRanges']: 680 | metadata['grantees'].append({'type':'cidr','value':grantee['CidrIp']}) 681 | elif len(outbound_rule['UserIdGroupPairs']) > 0: 682 | for grantee in outbound_rule['UserIdGroupPairs']: 683 | metadata['grantees'].append({'type':'sg','value':grantee['GroupId']}) 684 | outbound_rules.append(metadata) 685 | elif object_type == 'ACL': 686 | client = boto3.client('ec2') 687 | subnet_acl = client.describe_network_acls(Filters=[{'Name':'association.subnet-id','Values':[object_ids]}]) 688 | object_names = map(lambda x: x['Value'], filter(lambda y: y['Key'] == 'Name', subnet_acl['NetworkAcls'][0]['Tags'])) 689 | object_names = subnet_acl['NetworkAcls'][0]['NetworkAclId'] if len(object_names) == 0 else object_names 690 | for entry in subnet_acl['NetworkAcls'][0]['Entries']: 691 | metadata = {'rule_number':entry['RuleNumber']} 692 | if entry['Protocol'] == '-1': 693 | metadata['ip_protocol'] = 'all' 694 | metadata['port_range'] = {'from':0,'to':65535} 695 | else: 696 | if entry['Protocol'] == '1': 697 | metadata['ip_protocol'] = 'icmp' 698 | # Need to add port range logic 699 | elif entry['Protocol'] == '6': 700 | metadata['ip_protocol'] = 'tcp' 701 | metadata['port_range'] = {'from':entry['PortRange']['From'],'to':entry['PortRange']['To']} 702 | elif entry['Protocol'] == '17': 703 | metadata['ip_protocol'] = 'udp' 704 | # Need to add port range logic 705 | # else: 706 | # Need to add other protocol logic 707 | if entry['RuleAction'] == 'allow': 708 | metadata['allow'] = True 709 | else: 710 | metadata['allow'] = False 711 | metadata['grantees'] = [{'type':'cidr','value':entry['CidrBlock']}] 712 | if entry['Egress']: 713 | outbound_rules.append(metadata) 714 | elif not entry['Egress']: 715 | inbound_rules.append(metadata) 716 | return object_names,inbound_rules,outbound_rules 717 | 718 | 719 | def loop_through_rules(object_type,rule_list,port,target_ip=None,target_sgs=None,ephemeral_ports=None,ip_protocol='tcp'): 720 | traffic_allowed = False 721 | if object_type == 'ACL': 722 | for rule in sorted(rule_list, key=lambda acl_rule: acl_rule['rule_number']): 723 | match_found = False 724 | port_match = False 725 | if rule['ip_protocol'].lower() in ['all',ip_protocol.lower()]: 726 | if ephemeral_ports: 727 | logging.info("Checking if {}-{} in {}-{}".format(ephemeral_ports['from'],ephemeral_ports['to'],rule['port_range']['from'],rule['port_range']['to'])) 728 | port_match = rule['port_range']['from'] <= ephemeral_ports['from'] and rule['port_range']['to'] >= ephemeral_ports['to'] 729 | logging.info("port_match = {}".format(port_match)) 730 | else: 731 | logging.info("Checking if {} in {}-{}".format(port,rule['port_range']['from'],rule['port_range']['to'])) 732 | port_match = rule['port_range']['from'] <= port <= rule['port_range']['to'] 733 | logging.info("port_match = {}".format(port_match)) 734 | if port_match: 735 | for grantee in rule['grantees']: 736 | if target_ip['type'] == 'ip': 737 | if netaddr.IPAddress(target_ip['value']) in netaddr.IPNetwork(grantee['value']): 738 | if rule['allow']: 739 | traffic_allowed = True 740 | match_found = True 741 | logging.info("Traffic Allowed: {} in {}".format(target_ip['value'],grantee['value'])) 742 | break 743 | else: 744 | match_found = True 745 | break 746 | elif target_ip['type'] == 'cidr': 747 | if netaddr.IPNetwork(target_ip['value']) in netaddr.IPNetwork(grantee['value']): 748 | if rule['allow']: 749 | traffic_allowed = True 750 | match_found = True 751 | logging.info("Traffic Allowed: {} in {}".format(target_ip['value'],grantee['value'])) 752 | break 753 | else: 754 | match_found = True 755 | break 756 | if match_found: 757 | break 758 | elif object_type == 'SG': 759 | for rule in rule_list: 760 | if rule['ip_protocol'].lower() in ['all',ip_protocol.lower()]: 761 | if rule['port_range']['from'] <= port <= rule['port_range']['to']: 762 | for grantee in rule['grantees']: 763 | if grantee['type'] == 'sg': 764 | if grantee['value'] in target_sgs: 765 | traffic_allowed = True 766 | break 767 | elif grantee['type'] == 'cidr': 768 | if target_ip['type'] == 'ip': 769 | if netaddr.IPAddress(target_ip['value']) in netaddr.IPNetwork(grantee['value']): 770 | traffic_allowed = True 771 | break 772 | elif target_ip['type'] == 'cidr': 773 | if netaddr.IPNetwork(target_ip['value']) in netaddr.IPNetwork(grantee['value']): 774 | traffic_allowed = True 775 | break 776 | if traffic_allowed: 777 | break 778 | return traffic_allowed 779 | 780 | 781 | def get_ephemeral_ports(platform): 782 | try: 783 | ephemeral_index = config.ephemeral_index 784 | ephemeral_ports = ephemeral_index[platform.upper()] 785 | except: 786 | ephemeral_ports = {'from':1024,'to':65535} 787 | finally: 788 | return ephemeral_ports 789 | 790 | 791 | def format_list(the_list,article='and'): 792 | if len(the_list) > 1: 793 | result = "{} {} {}".format(", ".join(the_list[:-1]),article,the_list[-1]) 794 | elif len(the_list) == 1: 795 | result = the_list[0] 796 | else: 797 | result = None 798 | return result 799 | 800 | 801 | if __name__ == '__main__': 802 | if len(sys.argv) == 2: 803 | troubleshoot(sys.argv[1],sys.argv[2]) 804 | elif len(sys.argv) == 3: 805 | troubleshoot(sys.argv[1],sys.argv[2],sys.argv[3]) 806 | elif len(sys.argv) == 4: 807 | troubleshoot(sys.argv[1],sys.argv[2],sys.argv[3],sys.argv[4]) 808 | elif len(sys.argv) == 5: 809 | troubleshoot(sys.argv[1],sys.argv[2],sys.argv[3],sys.argv[4],sys.argv[5]) 810 | elif len(sys.argv) == 6: 811 | troubleshoot(sys.argv[1],sys.argv[2],sys.argv[3],sys.argv[4],sys.argv[5],sys.argv[6]) 812 | else: 813 | print('Usage:\n> {}'.format(config.usage)) --------------------------------------------------------------------------------