├── docs ├── .gitignore └── images │ ├── Architecture.png │ └── Perimeterator.png ├── setup.cfg ├── src ├── perimeterator │ ├── version.py │ ├── dispatcher │ │ ├── __init__.py │ │ └── sqs.py │ ├── scanner │ │ ├── __init__.py │ │ ├── exception.py │ │ └── nmap.py │ ├── enumerator │ │ ├── __init__.py │ │ ├── es.py │ │ ├── elb.py │ │ ├── rds.py │ │ ├── elbv2.py │ │ └── ec2.py │ ├── __init__.py │ └── helper.py ├── enumerator.py └── scanner.py ├── tox.ini ├── tests └── test_dispatcher_helper.py ├── terraform ├── outputs.tf ├── variables.tf ├── README.md └── main.tf ├── .gitignore ├── setup.py ├── LICENSE.txt ├── Dockerfile ├── CHANGELOG.md └── README.md /docs/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | [metadata] 5 | description-file = README.md 6 | -------------------------------------------------------------------------------- /src/perimeterator/version.py: -------------------------------------------------------------------------------- 1 | ''' Perimeterator version information. ''' 2 | 3 | __version__ = '0.3.0' 4 | -------------------------------------------------------------------------------- /docs/images/Architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkarnium/perimeterator/HEAD/docs/images/Architecture.png -------------------------------------------------------------------------------- /docs/images/Perimeterator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkarnium/perimeterator/HEAD/docs/images/Perimeterator.png -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py37 3 | 4 | [testenv] 5 | deps= 6 | pytest 7 | pytest-cov 8 | commands=pytest --cov=perimeterator 9 | -------------------------------------------------------------------------------- /src/perimeterator/dispatcher/__init__.py: -------------------------------------------------------------------------------- 1 | ''' Perimeterator - Enumerated address dispatchers. ''' 2 | 3 | from perimeterator.dispatcher import sqs # noqa: F401 4 | -------------------------------------------------------------------------------- /src/perimeterator/scanner/__init__.py: -------------------------------------------------------------------------------- 1 | ''' Perimeterator - Scanners. ''' 2 | 3 | from perimeterator.scanner import exception # noqa: F401 4 | from perimeterator.scanner import nmap # noqa: F401 5 | -------------------------------------------------------------------------------- /tests/test_dispatcher_helper.py: -------------------------------------------------------------------------------- 1 | ''' Implements tests for Dispatcher helpers. ''' 2 | 3 | import unittest 4 | 5 | import perimeterator 6 | 7 | 8 | class DispatcherHelperTestCase(unittest.TestCase): 9 | ''' Implements tests for Dispatcher helpers. ''' 10 | -------------------------------------------------------------------------------- /src/perimeterator/enumerator/__init__.py: -------------------------------------------------------------------------------- 1 | ''' Perimeterator - Enumerators. ''' 2 | 3 | from perimeterator.enumerator import ec2 # noqa: F401 4 | from perimeterator.enumerator import elb # noqa: F401 5 | from perimeterator.enumerator import elbv2 # noqa: F401 6 | from perimeterator.enumerator import rds # noqa: F401 7 | from perimeterator.enumerator import es # noqa: F401 8 | -------------------------------------------------------------------------------- /src/perimeterator/__init__.py: -------------------------------------------------------------------------------- 1 | ''' Perimeterator - Continuous AWS perimeter monitoring. ''' 2 | 3 | from perimeterator import helper # noqa: F401 4 | from perimeterator import scanner # noqa: F401 5 | from perimeterator import enumerator # noqa: F401 6 | from perimeterator import dispatcher # noqa: F401 7 | 8 | # Expose our version number somewhere expected. 9 | from perimeterator.version import __version__ 10 | -------------------------------------------------------------------------------- /src/perimeterator/scanner/exception.py: -------------------------------------------------------------------------------- 1 | ''' Perimeterator - Scanner exceptions. ''' 2 | 3 | 4 | class ScannerException(Exception): 5 | ''' Base exception for scanners. ''' 6 | 7 | 8 | class InvalidTargetException(ScannerException): 9 | ''' A target provided to a scanner was not valid. ''' 10 | 11 | 12 | class UnhandledScanException(ScannerException): 13 | ''' Something went wrong with the scan. ''' 14 | 15 | 16 | class TimeoutScanException(ScannerException): 17 | ''' The scan took too long, and was killed. ''' 18 | -------------------------------------------------------------------------------- /terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | // Render the Enumerate SQS queue ARN and region to the user when Terraform 2 | // has complete. 3 | output "ENUMERATOR_SQS_QUEUE" { 4 | value = "${aws_sqs_queue.enumerator.arn}" 5 | } 6 | 7 | // Render the Scanner SQS queue ARN and region to the user when Terraform 8 | // has complete. 9 | output "SCANNER_SQS_QUEUE" { 10 | value = "${aws_sqs_queue.scanner.arn}" 11 | } 12 | 13 | // Render the AWS Access Key Id to the user when Terraform has complete. 14 | output "AWS_ACCESS_KEY_ID" { 15 | value = "${aws_iam_access_key.scanner.id}" 16 | } 17 | 18 | // Render the AWS Secret Access Key to the user when Terraform has complete. 19 | output "AWS_SECRET_ACCESS_KEY" { 20 | value = "${aws_iam_access_key.scanner.secret}" 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .vscode 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # Installer logs 29 | pip-log.txt 30 | pip-delete-this-directory.txt 31 | 32 | # Unit test / coverage reports 33 | htmlcov/ 34 | .tox/ 35 | .coverage 36 | .coverage.* 37 | .cache 38 | nosetests.xml 39 | coverage.xml 40 | *,cover 41 | .hypothesis/ 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # VirtualEnv 48 | venv/ 49 | ENV/ 50 | 51 | # OS X Bullshit. 52 | .DS_Store 53 | 54 | # Packaged code. 55 | *.zip 56 | 57 | # Terraform files. 58 | *.tfstate* 59 | .terraform/ -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import find_packages, setup 3 | 4 | # Source the version from the package. 5 | __version__ = 'Unknown' 6 | exec(open('src/perimeterator/version.py').read()) 7 | 8 | setup( 9 | name='perimeterator', 10 | version=__version__, 11 | description='Continuous AWS Perimeter Monitoring', 12 | author='Peter Adkins', 13 | author_email='peter.adkins@kernelpicnic.net', 14 | url='https://www.github.com/darkarnium/perimeterator', 15 | packages=find_packages('src'), 16 | license='MIT', 17 | classifiers=[ 18 | 'License :: OSI Approved :: MIT License', 19 | 'Programming Language :: Python :: 3', 20 | ], 21 | package_dir={ 22 | 'perimeterator': 'src/perimeterator', 23 | }, 24 | scripts=[ 25 | 'src/enumerator.py', 26 | 'src/scanner.py', 27 | ], 28 | setup_requires=[ 29 | 'pytest-runner', 30 | ], 31 | tests_require=[ 32 | 'pytest', 33 | 'pytest-cov', 34 | ], 35 | install_requires=[ 36 | 'boto3==1.9.204' 37 | ] 38 | ) 39 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Peter Adkins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-stretch 2 | 3 | # Define environment varibles used to configure Perimeterator. This is, 4 | # currently, mainly used for documentation purposes. 5 | ARG AWS_ACCESS_KEY_ID 6 | ARG AWS_SECRET_ACCESS_KEY 7 | ARG ENUMERATOR_SQS_QUEUE 8 | ARG ENUMERATOR_SQS_REGION 9 | ARG SCANNER_SQS_QUEUE 10 | ARG SCANNER_SQS_REGION 11 | 12 | # Set a default region. 13 | ENV AWS_DEFAULT_REGION 'us-west-2' 14 | 15 | # Install dependencies and setup a home for Perimeterator. 16 | RUN apt-get update && \ 17 | apt-get upgrade -y && \ 18 | apt-get install -y nmap libcap2-bin && \ 19 | useradd -m -d /opt/perimeterator perimeterator && \ 20 | setcap cap_net_raw+ep /usr/bin/nmap 21 | 22 | # Install Perimeterator sources into the container. 23 | RUN mkdir -p /opt/perimeterator/src 24 | COPY --chown=perimeterator ./src /opt/perimeterator/src 25 | COPY --chown=perimeterator ./setup.* /opt/perimeterator/ 26 | 27 | # Install perimterator. 28 | WORKDIR /opt/perimeterator/ 29 | RUN pip3 install . && \ 30 | chown -R perimeterator: /opt/perimeterator 31 | 32 | # Kick off the monitor as soon as the container starts (by default). 33 | USER perimeterator 34 | ENTRYPOINT [ "python3", "/opt/perimeterator/src/scanner.py" ] 35 | -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | // Defines which region the Lambda functions will be deployed into. 2 | variable deployment_region { 3 | default = "us-west-2" 4 | } 5 | 6 | // The schedule on which to trigger the Enumerator. 7 | variable enumerator_schedule { 8 | default = "rate(24 hours)" 9 | } 10 | 11 | // A list of regions the Perimeterator Enumerator will scan. 12 | variable enumerator_regions { 13 | type = "list" 14 | default = [ 15 | "us-west-2", 16 | "us-west-1", 17 | "us-east-2", 18 | "us-east-1", 19 | "sa-east-1", 20 | "eu-west-3", 21 | "eu-west-2", 22 | "eu-west-1", 23 | "eu-north-1", 24 | "eu-central-1", 25 | "ca-central-1", 26 | "ap-southeast-2", 27 | "ap-southeast-1", 28 | "ap-south-1", 29 | "ap-northeast-2", 30 | "ap-northeast-1", 31 | ] 32 | } 33 | 34 | // The maximum amount of memory to allow the enumerator Lambda function to 35 | // consume. 36 | variable lambda_enumerator_memory_size { 37 | default = "256" 38 | } 39 | 40 | // The maximum duration that the enumerator Lambda function can run (timeout). 41 | variable lambda_enumerator_timeout { 42 | default = "300" 43 | } 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | The following list provides a brief overview of all features, bug fixes, and 4 | breaking changes as part of Perimeterator releases. 5 | 6 | ## 0.3.0 7 | 8 | ### 🤘Features 9 | 10 | * Adds AWS Elasticsearch Service support. 11 | * Single sourced versioning (`perimeterator.__version__`). 12 | * Bumped `boto3` version to latest. 13 | 14 | ### 🐛Bug Fixes 15 | 16 | * Fixed broken paths to `scripts` in `setup.py`. 17 | * Documentation update to indicate first `enumerator` run may need to be run 18 | manually, or it could take 24-hours for the first run to be executed. This 19 | is due to the `rate(24 hours)` CloudWatch Events schedule. 20 | 21 | ### 💥Breaking Changes 22 | 23 | * Nmap scanner now defaults to TCP only scans to greatly speed up scanning. 24 | * Changed installation paths inside `scanner` container. However, as the 25 | container entrypoint was updated to reflect new paths, no impact should be 26 | seen for the vast majority of users. 27 | 28 | ## 0.2.0 29 | 30 | ### 🤘Features 31 | 32 | * Added paging to enumerators in order to support accounts with a large 33 | number of resources. 34 | 35 | ### 🐛Bug Fixes 36 | 37 | * N/A 38 | 39 | ### 💥Breaking Changes 40 | 41 | * N/A 42 | 43 | ## 0.1.0 44 | 45 | ### 🤘Features 46 | 47 | * Initial Release. 48 | 49 | ### 🐛Bug Fixes 50 | 51 | * N/A 52 | 53 | ### 💥Breaking Changes 54 | 55 | * N/A 56 | -------------------------------------------------------------------------------- /src/perimeterator/dispatcher/sqs.py: -------------------------------------------------------------------------------- 1 | ''' Perimeterator - SQS dispatcher for enumerated addresses. ''' 2 | 3 | import logging 4 | import boto3 5 | import json 6 | 7 | from perimeterator.helper import aws_sqs_queue_url 8 | 9 | 10 | class Dispatcher(object): 11 | ''' Perimeterator - SQS dispatcher for enumerated addresses. ''' 12 | 13 | def __init__(self, queue): 14 | self.queue = aws_sqs_queue_url(queue) 15 | self.logger = logging.getLogger(__name__) 16 | self.client = boto3.client("sqs") 17 | 18 | def dispatch(self, account, resources): 19 | ''' Iterates over address array and enqueues for processing. ''' 20 | self.logger.info( 21 | "Attempting to enqueue %d resources", len(resources), 22 | ) 23 | for resource in resources: 24 | response = self.client.send_message( 25 | QueueUrl=self.queue, 26 | MessageAttributes={ 27 | "Identifier": { 28 | "DataType": "String", 29 | "StringValue": resource["identifier"], 30 | }, 31 | "Service": { 32 | "DataType": "String", 33 | "StringValue": resource["service"], 34 | } 35 | }, 36 | MessageBody=json.dumps(resource["addresses"]) 37 | ) 38 | self.logger.info( 39 | "Enqueued IPs for resource %s as %s", 40 | resource["identifier"], 41 | response["MessageId"], 42 | ) 43 | -------------------------------------------------------------------------------- /src/perimeterator/helper.py: -------------------------------------------------------------------------------- 1 | ''' Perimeterator - Internal helpers for Enumerator operations. ''' 2 | 3 | import boto3 4 | import socket 5 | 6 | 7 | def aws_elb_arn(region, name): 8 | ''' Constructs an ARN for an ELB in the given region. ''' 9 | return 'arn:aws:elasticloadbalancing:{0}:{1}:loadbalancer/{2}'.format( 10 | region, 11 | aws_account_id(), 12 | name, 13 | ) 14 | 15 | 16 | def aws_ec2_arn(region, identifier, resource='instance'): 17 | ''' Constructs an ARN for an EC2 resource in the given region. ''' 18 | return 'arn:aws:ec2:{0}:{1}:{2}/{3}'.format( 19 | region, 20 | aws_account_id(), 21 | resource, 22 | identifier, 23 | ) 24 | 25 | 26 | def dns_lookup(fqdn): 27 | ''' Lookups up the given FQDN returning an array of IPs. ''' 28 | addresses = [] 29 | try: 30 | # Provide a port number as '0' as we only want to trigger a DNS 31 | # lookup, no more, no less. 32 | for info in socket.getaddrinfo(fqdn, 0): 33 | addresses.append(info[4][0]) 34 | except socket.gaierror: 35 | # This is super janky, but hey. 36 | pass 37 | 38 | # Convert to a set and back again to deduplicate. 39 | return list(set(addresses)) 40 | 41 | 42 | def aws_sqs_queue_url(arn): 43 | ''' Convert an SQS ARN to SQS queue URL. ''' 44 | client = boto3.client('sqs') 45 | queue = client.get_queue_url( 46 | QueueName=arn.split(':')[-1], 47 | QueueOwnerAWSAccountId=arn.split(':')[-2], 48 | ) 49 | return queue['QueueUrl'] 50 | 51 | 52 | def aws_account_id(): 53 | ''' Attempts to get the account id for the current AWS account. ''' 54 | client = boto3.client('sts') 55 | return client.get_caller_identity()["Account"] 56 | -------------------------------------------------------------------------------- /src/perimeterator/enumerator/es.py: -------------------------------------------------------------------------------- 1 | ''' Perimeterator - Enumerator for AWS Elasticsearch instances. ''' 2 | 3 | import logging 4 | import boto3 5 | 6 | from perimeterator.helper import dns_lookup 7 | 8 | 9 | class Enumerator(object): 10 | ''' Perimeterator - Enumerator for AWS Elasticsearch instances. ''' 11 | # Required for Boto and reporting. 12 | SERVICE = 'es' 13 | 14 | def __init__(self, region): 15 | self.logger = logging.getLogger(__name__) 16 | self.region = region 17 | self.client = boto3.client(self.SERVICE, region_name=region) 18 | 19 | def get(self): 20 | ''' Attempt to get IPs associated with Elasticsearch endpoints. ''' 21 | resources = [] 22 | 23 | # Get a list of all domains and then manually prune it based on 24 | # whether the domains are created, being deleted, etc. 25 | candidates = self.client.list_domain_names() 26 | 27 | for es in candidates["DomainNames"]: 28 | # Query for data about this domain based on the enumerated name. 29 | domain = self.client.describe_elasticsearch_domain( 30 | DomainName=es["DomainName"] 31 | ) 32 | 33 | self.logger.debug("Inspecting ES domain %s", es["DomainName"]) 34 | if domain["DomainStatus"]["Created"] == False: 35 | self.logger.debug( 36 | "Skipping ES domain as it's still being provisioned" 37 | ) 38 | continue 39 | 40 | if domain["DomainStatus"]["Deleted"] == True: 41 | self.logger.debug( 42 | "Skipping ES domain as it's currently being deleted" 43 | ) 44 | continue 45 | 46 | # Skip VPC endpoints. 47 | if "Endpoints" in domain["DomainStatus"]: 48 | self.logger.debug( 49 | "Skipping ES domain as it has VPC only endpoints" 50 | ) 51 | continue 52 | 53 | # Lookup the DNS name to get the current IPs. 54 | try: 55 | resources.append({ 56 | "service": self.SERVICE, 57 | "identifier": domain["DomainStatus"]["ARN"], 58 | "addresses": dns_lookup( 59 | domain["DomainStatus"]["Endpoint"] 60 | ), 61 | }) 62 | except KeyError: 63 | self.logger.warning( 64 | "Skipping ES domain due to error when enumerating endpoints", 65 | ) 66 | 67 | self.logger.info("Got IPs for %s resources", len(resources)) 68 | return resources 69 | -------------------------------------------------------------------------------- /src/perimeterator/enumerator/elb.py: -------------------------------------------------------------------------------- 1 | ''' Perimeterator - Enumerator for AWS ELBs (Public IPs). ''' 2 | 3 | import logging 4 | import boto3 5 | 6 | from perimeterator.helper import aws_elb_arn 7 | from perimeterator.helper import dns_lookup 8 | 9 | 10 | class Enumerator(object): 11 | ''' Perimeterator - Enumerator for AWS ELBs (Public IPs). ''' 12 | # Required for Boto and reporting. 13 | SERVICE = 'elb' 14 | 15 | def __init__(self, region): 16 | self.logger = logging.getLogger(__name__) 17 | self.region = region 18 | self.client = boto3.client(self.SERVICE, region_name=region) 19 | 20 | def get(self): 21 | ''' Attempt to get all Public IPs from ELBs. ''' 22 | resources = [] 23 | 24 | # Iterate over results until AWS no longer returns a 'NextMarker' in 25 | # order to ensure all results are retrieved. 26 | marker = '' 27 | while marker is not None: 28 | # Unfortunately, Marker=None or Marker='' is invalid for this API 29 | # call, so it looks like we can't just set this to a None value, 30 | # or use a ternary here. 31 | if marker: 32 | candidates = self.client.describe_load_balancers( 33 | Marker=marker 34 | ) 35 | else: 36 | candidates = self.client.describe_load_balancers() 37 | 38 | # Check if we need to continue paging. 39 | if "NextMarker" in candidates: 40 | self.logger.debug( 41 | "'NextMarker' found, additional page of results to fetch" 42 | ) 43 | marker = candidates["NextMarker"] 44 | else: 45 | marker = None 46 | 47 | # For some odd reason the AWS API doesn't appear to allow a 48 | # filter on describe operations for ELBs, so we'll have to filter 49 | # manually. 50 | for elb in candidates["LoadBalancerDescriptions"]: 51 | self.logger.debug( 52 | "Inspecting ELB %s", elb["LoadBalancerName"], 53 | ) 54 | if elb["Scheme"] != "internet-facing": 55 | self.logger.debug("ELB is not internet facing") 56 | continue 57 | 58 | # Lookup the DNS name for this ELB to get the current IPs. We 59 | # also need to construct the ARN, as it's not provided in the 60 | # output from a describe operation (?!) 61 | resources.append({ 62 | "service": self.SERVICE, 63 | "identifier": aws_elb_arn( 64 | self.region, 65 | elb["LoadBalancerName"] 66 | ), 67 | "addresses": dns_lookup(elb["DNSName"]), 68 | }) 69 | 70 | self.logger.info("Got IPs for %s resources", len(resources)) 71 | return resources 72 | -------------------------------------------------------------------------------- /src/perimeterator/enumerator/rds.py: -------------------------------------------------------------------------------- 1 | ''' Perimeterator - Enumerator for AWS RDS instances (Public). ''' 2 | 3 | import logging 4 | import boto3 5 | 6 | from perimeterator.helper import dns_lookup 7 | 8 | 9 | class Enumerator(object): 10 | ''' Perimeterator - Enumerator for AWS RDS instances (Public). ''' 11 | # Required for Boto and reporting. 12 | SERVICE = 'rds' 13 | 14 | def __init__(self, region): 15 | self.logger = logging.getLogger(__name__) 16 | self.region = region 17 | self.client = boto3.client(self.SERVICE, region_name=region) 18 | 19 | def get(self): 20 | ''' Attempt to get all Public IPs from RDS instances. ''' 21 | resources = [] 22 | 23 | # Once again, the AWS API doesn't appear to allow filtering by RDS 24 | # instances where PubliclyAccessible is True. As a result, we'll 25 | # need to do this manually 26 | marker = '' 27 | while marker is not None: 28 | if marker: 29 | candidates = self.client.describe_db_instances(Marker=marker) 30 | else: 31 | candidates = self.client.describe_db_instances() 32 | 33 | # Check if we need to continue paging. 34 | if "Marker" in candidates: 35 | self.logger.debug( 36 | "'Marker' found, additional page of results to fetch" 37 | ) 38 | marker = candidates["Marker"] 39 | else: 40 | marker = None 41 | 42 | for rds in candidates["DBInstances"]: 43 | # Skip instances still being created as they may not yet have 44 | # endpoints created / generated. 45 | if rds["DBInstanceStatus"] == "creating": 46 | self.logger.debug( 47 | "Skipping instance as it's still being provisioned" 48 | ) 49 | continue 50 | 51 | self.logger.debug( 52 | "Inspecting RDS instance %s", 53 | rds["DBInstanceIdentifier"] 54 | ) 55 | if not rds["PubliclyAccessible"]: 56 | self.logger.debug("RDS instance is not internet facing") 57 | continue 58 | 59 | # Lookup the DNS name to get the current IPs. We're ignoring 60 | # the configured port for the time being, although this could 61 | # present a trivial optimisation for scanning speed up. 62 | try: 63 | resources.append({ 64 | "service": self.SERVICE, 65 | "identifier": rds["DBInstanceArn"], 66 | "addresses": dns_lookup(rds["Endpoint"]["Address"]), 67 | }) 68 | except KeyError: 69 | self.logger.warning( 70 | "Skipping RDS instance %s due to error when enumerating endpoints", 71 | rds["DBInstanceArn"] 72 | ) 73 | 74 | self.logger.info("Got IPs for %s resources", len(resources)) 75 | return resources 76 | -------------------------------------------------------------------------------- /src/enumerator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | ''' Perimeterator Enumerator. 3 | 4 | This wrapper is intended to allow for simplified AWS based deployment of the 5 | Perimeterator enumerator. This allows for a cost effective method of 6 | execution, as the Perimeterator poller component only needs to execute on a 7 | defined schedule in order to detect changes. 8 | ''' 9 | 10 | import os 11 | import logging 12 | import perimeterator 13 | 14 | # TODO: This should likely be configurable. 15 | MODULES = [ 16 | 'rds', 17 | 'ec2', 18 | 'elb', 19 | 'elbv2', 20 | 'es', 21 | ] 22 | 23 | 24 | def lambda_handler(event, context): 25 | ''' An AWS Lambda wrapper for the Perimeterator enumerator. ''' 26 | # Strip off any existing handlers that may have been installed by AWS. 27 | logger = logging.getLogger() 28 | for handler in logger.handlers: 29 | logger.removeHandler(handler) 30 | 31 | # Reconfigure the root logger the way we want it. 32 | logging.basicConfig( 33 | level=logging.INFO, 34 | format='%(asctime)s - %(process)d - [%(levelname)s] %(message)s' 35 | ) 36 | 37 | # Get the account id for the current AWS account. 38 | account = perimeterator.helper.aws_account_id() 39 | logger.info("Running in AWS account %s", account) 40 | 41 | # Get configurable options from environment variables. 42 | regions = os.getenv("ENUMERATOR_REGIONS", "us-west-2").split(",") 43 | sqs_queue = os.getenv("ENUMERATOR_SQS_QUEUE", None) 44 | logger.info("Configured results SQS queue is %s", sqs_queue) 45 | logger.info( 46 | "Configured regions for resource enumeration are %s", 47 | ", ".join(regions) 48 | ) 49 | 50 | # Setup the SQS dispatcher for submission of addresses to scanners. 51 | queue = perimeterator.dispatcher.sqs.Dispatcher(queue=sqs_queue) 52 | 53 | # Process regions one at a time, enumerating addresses for all configured 54 | # resources in the given region. Currently, it's not possible to only 55 | # enumerate different resources types by region. Maybe later! :) 56 | for region in regions: 57 | logger.info("Attempting to enumerate resources in %s", region) 58 | 59 | for module in MODULES: 60 | logger.info("Attempting to enumerate %s resources", module) 61 | try: 62 | # Ensure a handler exists for this type of resource. 63 | hndl = getattr(perimeterator.enumerator, module).Enumerator( 64 | region=region 65 | ) 66 | except AttributeError as err: 67 | logger.error( 68 | "Handler for %s resources not found, skipping: %s", 69 | module, 70 | err 71 | ) 72 | continue 73 | 74 | # Get all addresses and dispatch to SQS for processing. 75 | logger.info( 76 | "Submitting %s resources in %s for processing", 77 | module, 78 | region 79 | ) 80 | queue.dispatch(account, hndl.get()) 81 | 82 | 83 | if __name__ == '__main__': 84 | ''' Allow the script to be invoked outside of Lambda. ''' 85 | lambda_handler( 86 | dict(), # No real 'event' data. 87 | dict() # No real 'context' data. 88 | ) 89 | -------------------------------------------------------------------------------- /terraform/README.md: -------------------------------------------------------------------------------- 1 | # Perimeterator - Terraform 2 | 3 | This directory contains a set of Terraform configs which will deploy 4 | Perimeterator to AWS, and trigger the first "Enumeration" run. It will also 5 | schedule an "Enumeration" run to automatically take place every 24-hours. 6 | 7 | ## WARNING 8 | 9 | Deployment of these Terraform configs will create AWS resources that will 10 | incur cost! Please ensure you have familiarised yourself with the charges 11 | associated with the created resources before deployment. 12 | 13 | In addition to this, an IAM user for use with scanners external to AWS will 14 | be created as part of this deployment. The secret and access keys for this 15 | user are written to the `tfstate` file by Terraform. Please ensure to protect 16 | this state file appropriately, if required. 17 | 18 | Finally, **the author accepts no responsibility for errors and omissions in 19 | these Terraform configs which may yield unexpected behaviour and / or 20 | security misconfiguration.** 21 | 22 | ## Overview 23 | 24 | This directory contains Terraform configs for deploying Perimeterator. A 25 | non-exhaustive list of created resources is as follows: 26 | 27 | * Creation of SQS queue for scan requests (`enumerate`). 28 | * Creation of SQS queue for scan results (`scanner`). 29 | * Creation of SQS dead-letter queue for errors (`deadletter`). 30 | * Creation of Lambda functions to enumerate AWS resources. 31 | * Creation of IAM resources to allow Lambda functions to operate. 32 | * Creation of an IAM user for "external" scanners. 33 | * Creation of IAM keys for the created scanners. 34 | * Creation of IAM resources to allow "external" scanners functions to operate. 35 | * Creation of a CloudWatch Log Group for logging the output of Enumerator runs. 36 | 37 | ## Deployment 38 | 39 | To deploy simply follow the following steps: 40 | 41 | 1. `git clone` this repository to your machine. 42 | 2. Ensure AWS keys with the required permissions [are configured](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html#cli-quick-configuration). 43 | 3. Ensure [Terraform](https://learn.hashicorp.com/terraform/getting-started/install.html) is installed. 44 | 4. Change into the `terraform/` directory. 45 | 5. Execute `terraform plan`. 46 | 6. When ready to deploy, execute `terraform apply`. 47 | 7. Enjoy your ☕️ / 🍺 / 🥃 / 🍚 while you wait for the deployment to finish. 48 | 8. Use the output AWS IAM keys to deploy a scanner. 49 | 50 | Please be aware that the deployed `enumerator` Lambda function will only 51 | execute every 24 hours by default, and it may take some time for the first 52 | run to be triggered by the CloudWatch Events rule. Logs can be found in the 53 | CloudWatch console which can be used to monitor the status of `enumerator` 54 | runs. 55 | 56 | Invocation can also be performed manually after - if required - with the 57 | following AWS CLI command: 58 | 59 | ``` 60 | aws lambda invoke --function-name perimeterator-enumerator /dev/null 61 | ``` 62 | 63 | ## Logs 64 | 65 | When deployed, a new log group called `perimeterator-enumerator` will be 66 | created in CloudWatch. Logs of Enumerator runs can be found in this log 67 | group, easily accessible from CloudWatch Logs in the AWS console. 68 | 69 | ## Customisation 70 | 71 | In order to customise the deployment - such as changing the frequency of the 72 | Enumerator run, or region(s) on which it will enumerate resources - please 73 | see the `variables.tf` file, and the comments associated with the respective 74 | variable you would like to change. 75 | -------------------------------------------------------------------------------- /src/perimeterator/enumerator/elbv2.py: -------------------------------------------------------------------------------- 1 | ''' Perimeterator - Enumerator for AWS ELBv2 (Public IPs). ''' 2 | 3 | import boto3 4 | import logging 5 | 6 | from perimeterator.helper import dns_lookup 7 | 8 | 9 | class Enumerator(object): 10 | ''' Perimeterator - Enumerator for AWS ELBv2 (Public IPs). ''' 11 | # Required for Boto and reporting. 12 | SERVICE = 'elbv2' 13 | 14 | def __init__(self, region): 15 | self.logger = logging.getLogger(__name__) 16 | self.region = region 17 | self.client = boto3.client(self.SERVICE, region_name=region) 18 | 19 | def get(self): 20 | ''' Attempt to get all Public IPs from ELBv2 instances. ''' 21 | resources = [] 22 | 23 | # Iterate over results until AWS no longer returns a 'NextMarker' in 24 | # order to ensure all results are retrieved. 25 | marker = '' 26 | while marker is not None: 27 | # Unfortunately, Marker=None or Marker='' is invalid for this API 28 | # call, so it looks like we can't just set this to a None value, 29 | # or use a ternary here. 30 | if marker: 31 | candidates = self.client.describe_load_balancers( 32 | Marker=marker 33 | ) 34 | else: 35 | candidates = self.client.describe_load_balancers() 36 | 37 | # Check if we need to continue paging. 38 | if "NextMarker" in candidates: 39 | self.logger.debug( 40 | "'NextMarker' found, additional page of results to fetch" 41 | ) 42 | marker = candidates["NextMarker"] 43 | else: 44 | marker = None 45 | 46 | # For some odd reason the AWS API doesn't appear to allow a 47 | # filter on describe operations for ELBs, so we'll have to filter 48 | # manually. 49 | for elb in candidates["LoadBalancers"]: 50 | self.logger.debug( 51 | "Inspecting ELBv2 instance %s", elb["LoadBalancerArn"], 52 | ) 53 | if elb["Scheme"] != "internet-facing": 54 | self.logger.debug( 55 | "ELBv2 instance is not internet facing" 56 | ) 57 | continue 58 | 59 | # If a network load balancer, IPs will be present in the 60 | # describe output. If not, then we'll need to resolve the DNS 61 | # name to get the current LB IPs. 62 | if "LoadBalancerAddresses" in elb["AvailabilityZones"][0]: 63 | addresses = [] 64 | for az in elb["AvailabilityZones"]: 65 | # Each AZ has an associated IP allocation. 66 | for address in az["LoadBalancerAddresses"]: 67 | addresses.append(address["IpAddress"]) 68 | 69 | resources.append({ 70 | "service": self.SERVICE, 71 | "identifier": elb["LoadBalancerArn"], 72 | "addresses": addresses, 73 | }) 74 | else: 75 | resources.append({ 76 | "service": self.SERVICE, 77 | "identifier": elb["LoadBalancerArn"], 78 | "addresses": dns_lookup(elb["DNSName"]), 79 | }) 80 | 81 | self.logger.info("Got IPs for %s resources", len(resources)) 82 | return resources 83 | -------------------------------------------------------------------------------- /src/perimeterator/enumerator/ec2.py: -------------------------------------------------------------------------------- 1 | ''' Perimeterator - Enumerator for AWS EC2 Instances (Public IPs). ''' 2 | 3 | import logging 4 | import boto3 5 | 6 | from perimeterator.helper import aws_ec2_arn 7 | 8 | 9 | class Enumerator(object): 10 | ''' Perimeterator - Enumerator for AWS EC2 Instances (Public IPs). ''' 11 | # Required for Boto and reporting. 12 | SERVICE = 'ec2' 13 | 14 | def __init__(self, region): 15 | self.logger = logging.getLogger(__name__) 16 | self.region = region 17 | self.client = boto3.client(self.SERVICE, region_name=region) 18 | 19 | def get(self): 20 | ''' Attempt to get all Public IPs from EC2 instances. ''' 21 | resources = [] 22 | filters = [ 23 | { 24 | "Name": "instance-state-name", 25 | "Values": [ 26 | "pending", 27 | "running", 28 | ], 29 | } 30 | ] 31 | 32 | # Ensure that all IPs attached to running or pending instances are 33 | # accounted for. 34 | next_token = '' 35 | while next_token is not None: 36 | if next_token: 37 | candidates = self.client.describe_instances( 38 | Filters=filters, 39 | NextToken=next_token 40 | ) 41 | else: 42 | candidates = self.client.describe_instances( 43 | Filters=filters 44 | ) 45 | 46 | # Check if we need to continue paging. 47 | if "NextToken" in candidates: 48 | self.logger.debug( 49 | "'NextToken' found, additional page of results to fetch" 50 | ) 51 | next_token = candidates["NextToken"] 52 | else: 53 | next_token = None 54 | 55 | # A reservation can contain one or more instances 56 | for reservation in candidates["Reservations"]: 57 | self.logger.info( 58 | "Inspecting reservation %s", 59 | reservation["ReservationId"] 60 | ) 61 | for instance in reservation["Instances"]: 62 | self.logger.info( 63 | "Inspecting instance %s", instance["InstanceId"] 64 | ) 65 | # An instance can have multiple NICs. 66 | addresses = [] 67 | for nic in instance["NetworkInterfaces"]: 68 | # A NIC can have multiple IPs. 69 | for ip in nic["PrivateIpAddresses"]: 70 | # An IP may not have an association if it is only 71 | # an RFC1918 address. 72 | if "Association" in ip and "PublicIp" in ip["Association"]: 73 | addresses.append( 74 | ip["Association"]["PublicIp"] 75 | ) 76 | 77 | # We need to construct the EC2 instance ARN ourselves, as 78 | # this isn't provided as part of the describe output. 79 | resources.append({ 80 | "service": self.SERVICE, 81 | "identifier": aws_ec2_arn( 82 | self.region, 83 | instance["InstanceId"] 84 | ), 85 | "addresses": addresses, 86 | }) 87 | 88 | self.logger.info("Got IPs for %s resources", len(resources)) 89 | return resources 90 | -------------------------------------------------------------------------------- /src/perimeterator/scanner/nmap.py: -------------------------------------------------------------------------------- 1 | ''' Perimeterator - Port scanner (nmap). ''' 2 | 3 | import json 4 | import tempfile 5 | import ipaddress 6 | import subprocess 7 | import xml.etree.ElementTree as tree 8 | 9 | from perimeterator.scanner.exception import InvalidTargetException 10 | from perimeterator.scanner.exception import UnhandledScanException 11 | from perimeterator.scanner.exception import TimeoutScanException 12 | 13 | 14 | def _result_from_xml(xml, arn): 15 | ''' Converts Nmap XML format output to Perimeterator output format. ''' 16 | root = tree.fromstring(xml) 17 | 18 | # Extract the scan arguments from the XML, and track as metadata. 19 | metadata = { 20 | "scanner": root.attrib["scanner"], 21 | "arguments": root.attrib["args"], 22 | } 23 | 24 | # Extract results from the scan and append to the results document. 25 | results = dict() 26 | 27 | for host in root.iter("host"): 28 | # Key results by address. 29 | address = host.find("address").attrib["addr"] 30 | results[address] = [] 31 | 32 | # Append entries for ports not marked as 'closed'. 33 | for port in host.iter("port"): 34 | state = port.find("state").attrib["state"] 35 | if state != "closed": 36 | results[address].append({ 37 | "port": port.attrib["portid"], 38 | "state": state, 39 | "protocol": port.attrib["protocol"], 40 | }) 41 | 42 | # Serialise to JSON before returning. 43 | return json.dumps({ 44 | "metadata": metadata, 45 | "results": { 46 | arn: results 47 | } 48 | }) 49 | 50 | 51 | def run(arn, targets, timeout=300): 52 | ''' Runs a scan against the provided target(s). ''' 53 | # Write IPs to a temporary file - which will be passed to nmap - and 54 | # validate that all provided target addresses are valid. 55 | with tempfile.NamedTemporaryFile(mode='w+') as fin: 56 | for target in targets: 57 | try: 58 | ipaddress.ip_address(target) 59 | fin.write(target) 60 | fin.write("\n") 61 | except ValueError as err: 62 | raise InvalidTargetException(err) 63 | 64 | # Seek back to the start of the file, and flush to disk. 65 | fin.flush() 66 | fin.seek(0) 67 | 68 | # Attempt to kick off the scan, and timebox the run. 69 | with tempfile.NamedTemporaryFile(mode='w+') as fout: 70 | try: 71 | # TODO: Expose ability to customise nmap arguments. 72 | subprocess.run( 73 | [ 74 | "nmap", 75 | "--privileged", # Let nmap know it has caps. 76 | "-iL", fin.name, # Input hosts from file. 77 | "-oX", fout.name, # XML Output 78 | "--no-stylesheet", # Don't include XSL stylesheet. 79 | "-n", # Don't resolve DNS. 80 | "-T4", # Set timing to "Normal". 81 | "-Pn", # Treat hosts as online. 82 | "-sT", # Connect() scan. 83 | ], 84 | check=True, 85 | shell=False, 86 | timeout=timeout, 87 | stdout=subprocess.DEVNULL, 88 | stderr=subprocess.DEVNULL, 89 | ) 90 | except subprocess.CalledProcessError as err: 91 | raise UnhandledScanException(err) 92 | except subprocess.TimeoutExpired as err: 93 | raise TimeoutScanException(err) 94 | 95 | # Read back the file from the start, convert to our required 96 | # output format, and return it to our caller. 97 | fout.seek(0) 98 | return _result_from_xml(fout.read(), arn) 99 | -------------------------------------------------------------------------------- /src/scanner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | ''' Perimeterator Scanner - Scan resources identified by the Enumerator. ''' 3 | 4 | import os 5 | import json 6 | import boto3 7 | import logging 8 | 9 | import perimeterator 10 | 11 | # TODO: Move this to configuration. 12 | SCAN_TIMEOUT = 500 13 | 14 | 15 | def main(): 16 | ''' perimeterator scanner main thread. ''' 17 | # Strip off any existing handlers that may have already been installed. 18 | logger = logging.getLogger() 19 | for handler in logger.handlers: 20 | logger.removeHandler(handler) 21 | 22 | # Reconfigure the root logger the way we want it. 23 | logging.basicConfig( 24 | level=logging.INFO, 25 | format='%(asctime)s - %(process)d - [%(levelname)s] %(message)s' 26 | ) 27 | 28 | # Get the account id for the current AWS account. 29 | account = perimeterator.helper.aws_account_id() 30 | logger.info("Running against AWS account %s", account) 31 | 32 | # Get configurable options from environment variables. 33 | input_queue = perimeterator.helper.aws_sqs_queue_url( 34 | os.getenv("ENUMERATOR_SQS_QUEUE", None) 35 | ) 36 | output_queue = perimeterator.helper.aws_sqs_queue_url( 37 | os.getenv("SCANNER_SQS_QUEUE", None) 38 | ) 39 | logger.info("Configured input queue is %s", input_queue) 40 | logger.info("Configured output queue is %s", output_queue) 41 | 42 | # Setup I/O queues and start processing. 43 | sqs = boto3.client("sqs") 44 | 45 | logger.info("Starting message polling loop") 46 | while True: 47 | # Only process ONE message at a time, as parallelising the scan 48 | # operations is likely in larger environments. Further to this, set 49 | # the visibility timeout to the SCAN_TIMEOUT plus 15 seconds to 50 | # prevent a message from being 'requeued' / unhidden before a scan 51 | # has had time to complete - or timeout. 52 | queue = sqs.receive_message( 53 | QueueUrl=input_queue, 54 | WaitTimeSeconds=20, 55 | VisibilityTimeout=(SCAN_TIMEOUT + 15), 56 | MaxNumberOfMessages=1, 57 | MessageAttributeNames=["All"], 58 | ) 59 | try: 60 | messages = queue['Messages'] 61 | except KeyError: 62 | logger.debug("No messages in queue, re-polling") 63 | continue 64 | 65 | # Process messages, and kick off scan(s). 66 | # TODO: Fan-out via multiprocess to perform parallel scans? 67 | logger.info("Got %d messages from the queue", len(messages)) 68 | for i in range(0, len(messages)): 69 | # Ensure require attributes exist. 70 | try: 71 | handle = messages[i]['ReceiptHandle'] 72 | resource = messages[i]['MessageAttributes']['Identifier']['StringValue'] 73 | message_id = messages[i]['MessageId'] 74 | except KeyError as err: 75 | logger.error( 76 | "[%s] Required message attributes are missing: %s", 77 | message_id, 78 | err 79 | ) 80 | continue 81 | 82 | # Extract IPs from the message, and initiate scan. 83 | logger.info("[%s] Processing message body", message_id) 84 | try: 85 | targets = json.loads(messages[i]["Body"]) 86 | except json.decoder.JSONDecodeError as err: 87 | logger.error( 88 | "[%s] Message body appears malformed: %s", 89 | message_id, 90 | err, 91 | ) 92 | continue 93 | 94 | # Start the scan, and timebox it. 95 | try: 96 | logger.info( 97 | "[%s] Starting scan of resource %s", 98 | message_id, 99 | resource, 100 | ) 101 | scan_result = perimeterator.scanner.nmap.run( 102 | resource, 103 | targets, 104 | timeout=SCAN_TIMEOUT, 105 | ) 106 | except perimeterator.scanner.exception.TimeoutScanException: 107 | logger.error( 108 | "[%s] Scan timed out after %d seconds", 109 | message_id, 110 | SCAN_TIMEOUT, 111 | ) 112 | continue 113 | except perimeterator.scanner.exception.ScannerException as err: 114 | logger.error( 115 | "[%s] A scanner exception was encountered: %s", 116 | message_id, 117 | err, 118 | ) 119 | continue 120 | 121 | # Submit the results. 122 | # TODO: Dry move this into dispatcher and genericise. 123 | response = sqs.send_message( 124 | QueueUrl=output_queue, 125 | MessageAttributes=messages[i]['MessageAttributes'], 126 | MessageBody=scan_result, 127 | ) 128 | logger.info( 129 | "Enqueued scan results for resource %s as %s", 130 | resource, 131 | response["MessageId"], 132 | ) 133 | 134 | # Delete the processed message. 135 | logger.info("[%s] Message processed successfully", message_id) 136 | sqs.delete_message( 137 | QueueUrl=input_queue, 138 | ReceiptHandle=handle, 139 | ) 140 | 141 | 142 | if __name__ == '__main__': 143 | main() 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Perimeterator](./docs/images/Perimeterator.png?raw=true) 2 | 3 | 4 | Perimeterator is a small project intended to allow for continuous auditing 5 | of internet facing AWS services. It can be quickly deployed into AWS and will 6 | periodically enumerate internet-facing IP addresses for a number of commonly 7 | misconfigured AWS resources. 8 | 9 | The results from this enumeration process are pushed into a work queue for 10 | scanning by external scanner 'workers' in order to locate open network 11 | services. Scanner 'workers' can be deployed anywhere, and are intended to be 12 | deployed into non-trusted networks in order to provide a representation of 13 | access to services from the "general internet". 14 | 15 | Currently, the following AWS resource types are supported: 16 | 17 | * EC2 18 | * ELB 19 | * ELBv2 20 | * RDS 21 | * ES 22 | 23 | All communication between Perimeterator components occurs asynchronously 24 | through the use of AWS SQS queues. 25 | 26 | ![Architecture](./docs/images/Architecture.png?raw=true) 27 | 28 | ## Demo 29 | 30 | [![asciicast](https://asciinema.org/a/234948.svg)](https://asciinema.org/a/234948) 31 | 32 | ## Getting Started / Deployment 33 | 34 | Perimeterator requires a few components in order to function. However, in 35 | order to make getting started as easy as possible, a number of Terraform 36 | configs have been provided inside of the `terraform/` directory. 37 | 38 | To get started, please see the `terraform/README.md` file. 39 | 40 | ## Components 41 | 42 | Perimeterator has a number of components, due to its distributed nature. A 43 | brief overview of each of these components has been provided below. 44 | 45 | ### Enumerator (`enumerator.py`) 46 | 47 | This component is responsible for enumerating internet facing IP addresses 48 | which will be passed to downstream monitoring workers for scanning. This 49 | is intended to be run in Lambda, or somewhere inside of AWS which has access 50 | to perform the required "Describe" operations. 51 | 52 | As this is intended to be run in Lambda, configuration is currently only 53 | possible through environment variables. A brief summary of these exposed 54 | variables is as follows: 55 | 56 | * `ENUMERATOR_REGIONS` 57 | * A comma-delimited list of AWS regions to enumerate resources from. 58 | * This is set automatically if the provided Terraform configs are used. 59 | * `ENUMERATOR_SQS_QUEUE` 60 | * The URL of the SQS scan queue. 61 | * This is created automatically if the provided Terraform configs are used. 62 | 63 | ### Scanner (`scanner.py`) 64 | 65 | This component is responsible for performing scanning of the IPs enumerated 66 | by the Enumerator. This component should be run from an "untrusted" network 67 | in order to gain a better insight into exposure from the perspective of the 68 | "general internet". 69 | 70 | Currently, the Scanner only uses `nmap` with the [default `nmap-services`](https://nmap.org/book/man-port-specification.html) 71 | provided port range for TCP/UDP services. This is in order to prevent scans 72 | from taking an extremely long time to complete per host, at the cost of some 73 | accuracy in the case where uncommon ports are in use. This is likely to be 74 | made user configurable in the near future. 75 | 76 | An example `Dockerfile` for this component can be found in the root of this 77 | repository. As this component is likely not running inside of AWS, an IAM user 78 | and associated Access Key and Secret Key is created automatically for you if 79 | using the included Terraform configs for deployment. 80 | 81 | The following configuration is required to operate correctly. Once again, 82 | configuration is only possible through environment variables. A brief summary 83 | of these variables is as follows: 84 | 85 | * `AWS_DEFAULT_REGION` 86 | * The default AWS region to interact with. 87 | * This is set by default to `us-west-2`. 88 | * `AWS_ACCESS_KEY_ID` 89 | * The AWS access key associated with a user able to interact with SQS. 90 | * This is created automatically if the provided Terraform configs are used. 91 | * `AWS_SECRET_ACCESS_KEY` 92 | * The AWS secret key associated with a user able to interact with SQS. 93 | * This is created automatically if the provided Terraform configs are used. 94 | * `ENUMERATOR_SQS_QUEUE` 95 | * The URL of the SQS scan queue (input). 96 | * This is created automatically if the provided Terraform configs are used. 97 | * `SCANNER_SQS_QUEUE` 98 | * The URL of the SQS results queue (output). 99 | * This is created automatically if the provided Terraform configs are used. 100 | 101 | Building and executing this container can be performed by executing the 102 | following. Of course, the blank fields will need to be populated with the 103 | appropriate values. However, these match the names of the `outputs` from 104 | Terraform if Perimeterator is deployed using the provided Terraform configs. 105 | 106 | ``` 107 | docker build -t perimeterator-scanner:master . 108 | docker run \ 109 | -e AWS_ACCESS_KEY_ID= \ 110 | -e AWS_SECRET_ACCESS_KEY= \ 111 | -e SCANNER_SQS_QUEUE= \ 112 | -e ENUMERATOR_SQS_QUEUE= \ 113 | perimeterator-scanner:master 114 | ``` 115 | 116 | ### Notify (`notify.py`) 117 | 118 | This component is in progress, but is not yet complete. 119 | 120 | ## Results 121 | 122 | Result data from scans is currently written to an SQS queue ready for 123 | downstream processing. Fetching and processing this data is still left as an 124 | "exercise for the reader", however, a reporting mechanism which consumes this 125 | data and generates a "diff" of results is actively being worked on. 126 | 127 | The format of the output data, currently, is as follows: 128 | 129 | ``` 130 | { 131 | "metadata": { 132 | "scanner": "nmap", 133 | "arguments": "-Pn -sT -sU -T4 -n", 134 | }, 135 | "results": { 136 | "arn:aws:ec2:12345678:instance/i-coffee": { 137 | "192.0.2.0": [ 138 | { 139 | "port": "22", 140 | "state": "open", 141 | "protocol": "tcp" 142 | }, 143 | { 144 | "port": "80", 145 | "state": "open", 146 | "protocol": "tcp" 147 | } 148 | ] 149 | } 150 | } 151 | } 152 | ``` 153 | 154 | Further to this, and if required, the ARN of the resource from which the 155 | scanned address was found is present in the SQS message attributes as a 156 | string value named `Identifier`. 157 | -------------------------------------------------------------------------------- /terraform/main.tf: -------------------------------------------------------------------------------- 1 | 2 | // Deployment will be into AWS. Leave Terraform to locate credentials via 3 | // usual AWS SDK methods - environment variables, configuration files, or 4 | // instance roles, etc. 5 | provider "aws" { 6 | region = "${var.deployment_region}" 7 | } 8 | 9 | // Zip up the `src` directory, to produce a Lambda compatible deployment 10 | // artifact. Code that runs in Lambda only requires boto3, which is installed 11 | // by default so there is no need to include setup.py and co. 12 | data "archive_file" "deployment" { 13 | type = "zip" 14 | output_path = "${path.module}/perimeterator.zip" 15 | source_dir = "${path.module}/../src/" 16 | } 17 | 18 | // Create a perimeterator dead letter queue. 19 | resource "aws_sqs_queue" "deadletter" { 20 | name = "perimeterator-deadletter" 21 | message_retention_seconds = 86400 22 | max_message_size = 1024 23 | } 24 | 25 | // Create a perimeterator enumerator queue. 26 | resource "aws_sqs_queue" "enumerator" { 27 | name = "perimeterator-enumerator" 28 | message_retention_seconds = 3600 29 | max_message_size = 1024 30 | redrive_policy = <