├── setup.cfg ├── tests ├── __init__.py ├── test_main.py ├── base │ ├── __init__.py │ ├── test_output.py │ ├── test_validation.py │ ├── test_runner.py │ └── test_cfn_resources.py └── resources │ ├── __init__.py │ ├── test_AWS_EC2_Instance.py │ ├── test_AWS_EFS_FileSystem.py │ ├── test_AWS_KMS_Key.py │ ├── test_AWS_IAM_Role.py │ ├── test_AWS_DynamoDB_Table.py │ ├── test_AWS_S3_Bucket.py │ └── test_AWS_Lambda_Function.py ├── cfn_sweeper ├── __init__.py ├── base │ ├── __init__.py │ ├── output.py │ ├── runner.py │ └── cfn_resources.py ├── resources │ ├── __init__.py │ ├── AWS_KMS_Key.py │ ├── AWS_EFS_FileSystem.py │ ├── AWS_IAM_Role.py │ ├── AWS_Lambda_Function.py │ ├── AWS_EC2_Instance.py │ ├── AWS_DynamoDB_Table.py │ ├── AWS_S3_Bucket.py │ └── base.py ├── artwork.py ├── validation.py └── main.py ├── pyproject.toml ├── Pipfile ├── setup.py ├── .github └── workflows │ ├── bandit.yml │ ├── snyk.yml │ └── python_ci.yml ├── LICENSE ├── .gitignore ├── README.md └── Pipfile.lock /setup.cfg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cfn_sweeper/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/base/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cfn_sweeper/base/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/resources/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cfn_sweeper/resources/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | boto3 = "*" 8 | pyyaml = "*" 9 | pyfiglet = "*" 10 | 11 | [dev-packages] 12 | pytest = "*" 13 | pylint = "*" 14 | pytest-mock = "*" 15 | moto = {extras = ["all"], version = "*"} 16 | autopep8 = "*" 17 | flake8 = "*" 18 | pytest-watch = "*" 19 | bandit = "*" 20 | jeepney = "*" 21 | twine = "*" 22 | build = "*" 23 | 24 | [requires] 25 | python_version = "3.8" 26 | -------------------------------------------------------------------------------- /cfn_sweeper/resources/AWS_KMS_Key.py: -------------------------------------------------------------------------------- 1 | from cfn_sweeper.resources.base import base_resource 2 | import boto3 3 | 4 | class resource(base_resource): 5 | 6 | def __init__(self): 7 | self.resource_name = "AWS::KMS::Key" 8 | 9 | def gather(self, region): 10 | results = [] 11 | kms = boto3.client('kms', region_name=region) 12 | paginator = kms.get_paginator('list_keys') 13 | for response in paginator.paginate(): 14 | response_key_ids = [r.get('KeyId') for r in response['Keys']] 15 | results.extend(response_key_ids) 16 | return results -------------------------------------------------------------------------------- /cfn_sweeper/artwork.py: -------------------------------------------------------------------------------- 1 | from re import X 2 | from pyfiglet import Figlet 3 | class Artwork(): 4 | 5 | def art(): 6 | f = Figlet(font='larry3d') 7 | print("\n https://github.com/rileydakota/cfn-sweeper" ) 8 | print (f.renderText('CFN SWEEPER \n')) 9 | print((" The umanaged resource detector tool!").center(20)) 10 | print(("-----------------------------------------------------------").center(24, " ")) 11 | print((" Run Report").center(20)) 12 | print(("-----------------------------------------------------------").center(24, " ")) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | VERSION = '0.0.6' 4 | DESCRIPTION = 'A tool for finding resources unmanaged by cloudformation' 5 | 6 | 7 | setup( 8 | name="cfn-sweeper", 9 | version=VERSION, 10 | py_modules=["main"], 11 | entry_points={ 12 | 'console_scripts': [ 13 | 'cfn_sweeper=cfn_sweeper.main:main', 14 | ], 15 | }, 16 | description=DESCRIPTION, 17 | packages=find_packages(), 18 | install_requires=[ 19 | 'boto3', 20 | 'wheel', 21 | 'pyyaml', 22 | 'pyfiglet' 23 | ], 24 | python_requires=">=3.8" 25 | 26 | ) 27 | -------------------------------------------------------------------------------- /cfn_sweeper/resources/AWS_EFS_FileSystem.py: -------------------------------------------------------------------------------- 1 | from cfn_sweeper.resources.base import base_resource 2 | import boto3 3 | 4 | class resource(base_resource): 5 | 6 | def __init__(self): 7 | self.resource_name = "AWS::EFS::FileSystem" 8 | 9 | def gather(self, region: str): 10 | results = [] 11 | efs = boto3.client('efs', region_name=region) 12 | fs_paginator = efs.get_paginator('describe_file_systems') 13 | for response in fs_paginator.paginate(): 14 | response_fs_names = [r.get('FileSystemId') for r in response['FileSystems']] 15 | results.extend(response_fs_names) 16 | return results -------------------------------------------------------------------------------- /cfn_sweeper/resources/AWS_IAM_Role.py: -------------------------------------------------------------------------------- 1 | from cfn_sweeper.resources.base import base_resource 2 | import boto3 3 | 4 | class resource(base_resource): 5 | 6 | def __init__(self): 7 | self.resource_name = "AWS::IAM::Role" 8 | 9 | def gather(self, region): 10 | results = [] 11 | iam = boto3.client('iam', region_name=region) 12 | role_paginator = iam.get_paginator('list_roles') 13 | for response in role_paginator.paginate(): 14 | response_role_names = [r.get('RoleName') for r in response['Roles']] 15 | results.extend(response_role_names) 16 | return results 17 | 18 | 19 | -------------------------------------------------------------------------------- /cfn_sweeper/resources/AWS_Lambda_Function.py: -------------------------------------------------------------------------------- 1 | from cfn_sweeper.resources.base import base_resource 2 | import boto3 3 | 4 | class resource(base_resource): 5 | 6 | def __init__(self): 7 | self.resource_name = "AWS::Lambda::Function" 8 | 9 | def gather(self, region): 10 | results = [] 11 | client = boto3.client('lambda', region_name=region) 12 | function_paginator = client.get_paginator('list_functions') 13 | for response in function_paginator.paginate(): 14 | response_function_names = [r.get('FunctionName') for r in response['Functions']] 15 | results.extend(response_function_names) 16 | return results -------------------------------------------------------------------------------- /cfn_sweeper/resources/AWS_EC2_Instance.py: -------------------------------------------------------------------------------- 1 | from cfn_sweeper.resources.base import base_resource 2 | import boto3 3 | 4 | class resource(base_resource): 5 | 6 | def __init__(self): 7 | self.resource_name = "AWS::EC2::Instance" 8 | 9 | #Can return the below as well if needed: , "Platform:": instance.platform, "Type:": instance.instance_type, "PublicIPv4:": instance.public_ip_address, "AMI:": #instance.image.id, "State:": instance.state 10 | def gather(self, region): 11 | result = [] 12 | ec2 = boto3.resource('ec2',region_name=region) 13 | for instance in ec2.instances.all(): 14 | result.append(instance.id) 15 | return result 16 | -------------------------------------------------------------------------------- /.github/workflows/bandit.yml: -------------------------------------------------------------------------------- 1 | name: Bandit 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | Security-SAST: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: [3.8] 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Install dependencies with pipenv 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install pipenv 22 | pipenv install --deploy --dev 23 | - name: SAST - Bandit 24 | run: | 25 | pipenv run bandit ./cfn_sweeper/* -r -------------------------------------------------------------------------------- /.github/workflows/snyk.yml: -------------------------------------------------------------------------------- 1 | name: Snyk 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | Security-SCA: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: [3.8] 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Install dependencies with pipenv 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install pipenv 22 | pipenv install --deploy --dev 23 | - name: SCA - Snyk 24 | uses: snyk/actions/python-3.8@master 25 | env: 26 | SNYK_TOKEN: ${{ secrets.SNYK_API_KEY }} -------------------------------------------------------------------------------- /tests/resources/test_AWS_EC2_Instance.py: -------------------------------------------------------------------------------- 1 | import cfn_sweeper.resources.AWS_EC2_Instance as ec2 2 | import pytest 3 | import boto3 4 | from moto import mock_ec2 5 | 6 | @mock_ec2 7 | def test_ec2_resource(): 8 | ec2_scanner_resource = ec2.resource() 9 | 10 | scan_result = ec2_scanner_resource.gather('us-east-1') 11 | assert len(scan_result) == 0 12 | 13 | client = boto3.resource('ec2', region_name='us-east-1') 14 | client.create_instances( 15 | ImageId= 'aki-00806369', 16 | MinCount=1, 17 | MaxCount=1, 18 | InstanceType='t2.micro', 19 | ) 20 | 21 | 22 | assert ec2_scanner_resource.resource_name == 'AWS::EC2::Instance' 23 | 24 | scan_result = ec2_scanner_resource.gather(region='us-east-1') 25 | 26 | assert len(scan_result) == 1 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /cfn_sweeper/resources/AWS_DynamoDB_Table.py: -------------------------------------------------------------------------------- 1 | from cfn_sweeper.resources.base import base_resource 2 | import boto3 3 | 4 | class resource(base_resource): 5 | 6 | def __init__(self): 7 | self.resource_name = "AWS::DynamoDB::Table" 8 | 9 | 10 | def gather(self, region): 11 | result = [] 12 | dynamodb = boto3.client('dynamodb',region_name=region) 13 | response = dynamodb.list_tables() 14 | for table in response['TableNames']: 15 | result.append(table) 16 | 17 | while 'LastEvaluatedTableName' in response: 18 | 19 | response = dynamodb.list_tables(ExclusiveStartTableName = response['LastEvaluatedTableName']) 20 | for i in response['TableNames']: 21 | result.append(i) 22 | else: 23 | return result 24 | -------------------------------------------------------------------------------- /cfn_sweeper/resources/AWS_S3_Bucket.py: -------------------------------------------------------------------------------- 1 | from cfn_sweeper.resources.base import base_resource 2 | import boto3 3 | 4 | class resource(base_resource): 5 | 6 | def __init__(self): 7 | self.resource_name = "AWS::S3::Bucket" 8 | 9 | def gather(self, region): 10 | results = [] 11 | s3_client = boto3.client('s3', region_name=None) 12 | raw_response = s3_client.list_buckets() 13 | for bucket in raw_response['Buckets']: 14 | bucket_region = s3_client.get_bucket_location(Bucket=bucket['Name'])['LocationConstraint'] 15 | if region == 'us-east-1': 16 | if bucket_region == None: 17 | results.append(bucket['Name']) 18 | elif bucket_region == region: 19 | results.append(bucket['Name']) 20 | 21 | 22 | return results 23 | -------------------------------------------------------------------------------- /tests/resources/test_AWS_EFS_FileSystem.py: -------------------------------------------------------------------------------- 1 | import cfn_sweeper.resources.AWS_EFS_FileSystem as efs 2 | import pytest 3 | import boto3 4 | from moto import mock_efs 5 | 6 | @mock_efs 7 | def test_efs_resource(): 8 | 9 | efs_fs_scanner_resource = efs.resource() 10 | 11 | assert efs_fs_scanner_resource.resource_name == 'AWS::EFS::FileSystem' 12 | 13 | scan_result = efs_fs_scanner_resource.gather('us-east-1') 14 | assert len(scan_result) == 0 15 | 16 | client = boto3.client('efs', region_name='us-east-1') 17 | client.create_file_system() 18 | 19 | assert len(efs_fs_scanner_resource.gather(region='us-east-1')) == 1 20 | assert len(efs_fs_scanner_resource.gather(region='us-east-2')) == 0 21 | 22 | for x in range(0,100): 23 | client.create_file_system() 24 | 25 | assert len(efs_fs_scanner_resource.gather(region='us-east-1')) == 101 26 | -------------------------------------------------------------------------------- /.github/workflows/python_ci.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: [3.8] 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Install dependencies with pipenv 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install pipenv 22 | pipenv install --deploy --dev 23 | - name: Lint with flake8 24 | run: | 25 | pipenv run flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 26 | - name: Test with pytest 27 | run: | 28 | pipenv run pytest -------------------------------------------------------------------------------- /tests/base/test_output.py: -------------------------------------------------------------------------------- 1 | from cfn_sweeper.base.output import ScanReport 2 | import pytest 3 | 4 | def test_ScanReport(capsys): 5 | 6 | report = ScanReport() 7 | report.add_resource_results(resource_type='AWS::S3::Bucket', managed_resources=['mycfnmadebucket'], unmanaged_resources=['mynoncfnbucket']) 8 | 9 | assert report._ScanReport__report_data == { 10 | 'AWS::S3::Bucket':{ 11 | 'managed':['mycfnmadebucket'], 12 | 'unmanaged':['mynoncfnbucket'] 13 | } 14 | } 15 | 16 | report.print_to_json() 17 | captured = capsys.readouterr() 18 | assert captured.out == '{"AWS::S3::Bucket": {"managed": ["mycfnmadebucket"], "unmanaged": ["mynoncfnbucket"]}}\n' 19 | 20 | report.print_to_yaml() 21 | captured = capsys.readouterr() 22 | assert captured.out == 'AWS::S3::Bucket:\n managed:\n - mycfnmadebucket\n unmanaged:\n - mynoncfnbucket\n\n' 23 | 24 | report.print_to_stdout() 25 | captured = capsys.readouterr() 26 | assert captured.out == 'mynoncfnbucket\n' -------------------------------------------------------------------------------- /cfn_sweeper/resources/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | class base_resource(ABC): 4 | """ 5 | Abstract class for adding individual resource scanning support to cfn_sweeper 6 | All classes built from this require the resource_name property to be defined, 7 | and a gather() method to be implemented on the subclass. 8 | 9 | The resource_name property should be the cloudformation type of the resource 10 | (eg AWS::S3::Bucket). 11 | 12 | The gather() method should take a region property, and return a list of all physical 13 | resource ids for the resource type in question (for S3 Buckets - given the region of 14 | us-east-1, it would return a list of all the bucket names in us-east-1) 15 | """ 16 | def resource_name(self): 17 | try: 18 | return self._resource_name 19 | except AttributeError: 20 | raise NotImplementedError('base_resources are required to have a resource name set') 21 | 22 | @abstractmethod 23 | def gather(self, region:str): 24 | pass -------------------------------------------------------------------------------- /cfn_sweeper/base/output.py: -------------------------------------------------------------------------------- 1 | import json 2 | import yaml 3 | from pprint import pprint 4 | from cfn_sweeper.artwork import Artwork 5 | class ScanReport(): 6 | def __init__(self): 7 | self.__report_data = {} 8 | 9 | def add_resource_results(self, resource_type:str, managed_resources:list, unmanaged_resources:list): 10 | self.__report_data[resource_type] = { 11 | 'managed': managed_resources, 12 | 'unmanaged': unmanaged_resources 13 | } 14 | 15 | def print_to_yaml(self): 16 | print(yaml.dump(self.__report_data)) 17 | 18 | def print_to_json(self): 19 | print(json.dumps(self.__report_data)) 20 | 21 | def print_to_pretty(self): 22 | Artwork.art() 23 | pprint(self.__report_data) 24 | 25 | 26 | def print_to_stdout(self): 27 | result = [] 28 | 29 | for key in self.__report_data.keys(): 30 | result.extend(self.__report_data[key]['unmanaged']) 31 | 32 | 33 | for unmanaged_item in result: 34 | print(unmanaged_item) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 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. -------------------------------------------------------------------------------- /tests/resources/test_AWS_KMS_Key.py: -------------------------------------------------------------------------------- 1 | import cfn_sweeper.resources.AWS_KMS_Key as kms 2 | import pytest 3 | import json 4 | import boto3 # AWS SDK for Python 5 | from moto import mock_kms 6 | 7 | 8 | @mock_kms 9 | def test_kms(): 10 | 11 | kms_scanner_resource = kms.resource() 12 | 13 | scan_result = kms_scanner_resource.gather('us-east-1') 14 | assert len(scan_result) == 0 15 | 16 | kms_client = boto3.client('kms', region_name='us-east-1') 17 | kmscnt = 0 18 | while kmscnt < 10: 19 | kmscnt += 1 20 | kms_client.create_key() 21 | 22 | scan_result_us1 = kms_scanner_resource.gather('us-east-1') 23 | assert len(scan_result_us1) == 10 24 | 25 | 26 | scan_result_us2 = kms_scanner_resource.gather('us-east-2') 27 | assert len(scan_result_us2) == 0 28 | 29 | kms_client_other_region = boto3.client('kms', region_name='us-east-2') 30 | kmscnt = 0 31 | while kmscnt < 10: 32 | kmscnt += 1 33 | kms_client_other_region.create_key() 34 | 35 | scan_result_us1 = kms_scanner_resource.gather('us-east-1') 36 | scan_result_us2 = kms_scanner_resource.gather('us-east-2') 37 | assert len(scan_result_us1) == 10 38 | assert len(scan_result_us2) == 10 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /cfn_sweeper/validation.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import re 3 | import argparse 4 | 5 | class ValidateRegion(argparse.Action): 6 | """Validate Region""" 7 | def __call__(self, parser, namespace, values, option_string=None): 8 | regex = re.compile('(us(-gov)?|ap|ca|cn|eu|sa)-(central|(north|south)?(east|west)?)-\d', re.I) 9 | match = regex.match(str(values)) 10 | if bool(match) == False: 11 | sys.exit("--region parameter must be listed in the code portion of this table: https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints") 12 | else: 13 | setattr(namespace, self.dest, values) 14 | 15 | class Validateoutput(argparse.Action): 16 | """Validate Output""" 17 | def __call__(self, parser, namespace, values, option_string=None): 18 | output = ['pretty', 'json', 'yaml','stdout'] 19 | if not values in output: 20 | sys.exit("--output parameter currently only supports: " + str(output)) 21 | else: 22 | setattr(namespace, self.dest, values) 23 | 24 | class Validatefilter(argparse.Action): 25 | """Validate Filter Types""" 26 | def __call__(self, parser, namespace, values, option_string=None): 27 | regex = re.compile('AWS::[0-z]*::[0-z]*', re.I) 28 | for value in values: 29 | match = regex.match(str(value)) 30 | if match: 31 | setattr(namespace, self.dest, values) 32 | else: 33 | sys.exit("--filter-types parameter not valid, please look at the README and check: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html ") 34 | -------------------------------------------------------------------------------- /tests/resources/test_AWS_IAM_Role.py: -------------------------------------------------------------------------------- 1 | # test_AWS_IAM_Role.py 2 | import cfn_sweeper.resources.AWS_IAM_Role as iam 3 | import pytest 4 | import json 5 | import boto3 # AWS SDK for Python 6 | from moto import mock_iam # since we're going to mock DynamoDB service 7 | 8 | @mock_iam 9 | def test_iam(): 10 | 11 | iam_scanner_resource = iam.resource() 12 | 13 | 14 | scan_result = iam_scanner_resource.gather('us-east-1') 15 | assert len(scan_result) == 0 16 | 17 | """ 18 | Create and mock role creation and trust relation 19 | """ 20 | 21 | assume_role_policy_document = json.dumps({ 22 | "Version": "2012-10-17", 23 | "Statement": [ 24 | { 25 | "Effect": "Allow", 26 | "Principal": { 27 | "Service": "support.amazonaws.com" 28 | }, 29 | "Action": "sts:AssumeRole" 30 | } 31 | ] 32 | }) 33 | 34 | 35 | roles = boto3.client('iam', region_name='us-east-1') 36 | rolecnt = 0 37 | while rolecnt < 10: 38 | rolecnt += 1 39 | roles.create_role( 40 | RoleName='RoleName' + str(rolecnt), 41 | AssumeRolePolicyDocument = assume_role_policy_document, 42 | Description='RoleName' + str(rolecnt), 43 | Tags=[ 44 | { 45 | 'Key': 'mock_role', 46 | 'Value': 'mock_role' + str(rolecnt) 47 | }, 48 | ] 49 | ) 50 | 51 | 52 | """ 53 | Test if our mock role(s) are ready and we can return it 54 | """ 55 | 56 | assert iam_scanner_resource.resource_name == 'AWS::IAM::Role' 57 | 58 | 59 | scan_result = iam_scanner_resource.gather(region='us-east-1') 60 | 61 | assert len(scan_result) == rolecnt 62 | 63 | assert any("RoleName" in s for s in scan_result) 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /tests/resources/test_AWS_DynamoDB_Table.py: -------------------------------------------------------------------------------- 1 | # test_AWS_DynamoDB_Table.py 2 | import cfn_sweeper.resources.AWS_DynamoDB_Table as dynamo 3 | import pytest 4 | import boto3 # AWS SDK for Python 5 | from moto import mock_dynamodb2 # since we're going to mock DynamoDB service 6 | 7 | @mock_dynamodb2 8 | def test_dynamo_db(): 9 | 10 | dynamo_scanner_resource = dynamo.resource() 11 | 12 | 13 | scan_result = dynamo_scanner_resource.gather('us-east-1') 14 | assert len(scan_result) == 0 15 | 16 | """ 17 | Create database resource and mock table 18 | """ 19 | dynamodb = boto3.client('dynamodb', region_name='us-east-1') 20 | dbcnt = 0 21 | while dbcnt < 105: 22 | dbcnt += 1 23 | dynamodb.create_table(TableName="TableName" + str(dbcnt), 24 | KeySchema=[ 25 | { 26 | 'AttributeName': 'Test', 27 | 'KeyType': 'HASH' 28 | } 29 | ], 30 | AttributeDefinitions=[ 31 | { 32 | 'AttributeName': 'Test', 33 | 'AttributeType': 'N' 34 | } 35 | ], 36 | ProvisionedThroughput={ 37 | 'ReadCapacityUnits': 1, 38 | 'WriteCapacityUnits': 1 39 | } 40 | ) 41 | 42 | 43 | """ 44 | Test if our mock table is ready and we can return it 45 | """ 46 | 47 | assert dynamo_scanner_resource.resource_name == 'AWS::DynamoDB::Table' 48 | 49 | 50 | scan_result = dynamo_scanner_resource.gather(region='us-east-1') 51 | 52 | 53 | assert len(scan_result) == dbcnt 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /tests/resources/test_AWS_S3_Bucket.py: -------------------------------------------------------------------------------- 1 | import cfn_sweeper.resources.AWS_S3_Bucket as bucket 2 | import pytest 3 | import boto3 4 | from moto import mock_s3 5 | 6 | @mock_s3 7 | def test_s3_resource(): 8 | s3_scanner_resource = bucket.resource() 9 | 10 | scan_result = s3_scanner_resource.gather('us-east-1') 11 | assert len(scan_result) == 0 12 | 13 | client = boto3.client('s3', region_name='us-east-1') 14 | client.create_bucket(Bucket='bucket1') 15 | client.create_bucket(Bucket='bucket2') 16 | 17 | assert s3_scanner_resource.resource_name == 'AWS::S3::Bucket' 18 | 19 | scan_result = s3_scanner_resource.gather(region='us-east-1') 20 | assert len(scan_result) == 2 21 | assert 'bucket1' in scan_result 22 | assert 'bucket2' in scan_result 23 | 24 | client.delete_bucket(Bucket='bucket1') 25 | client.delete_bucket(Bucket='bucket2') 26 | 27 | # test pagination 28 | for num in range(500): 29 | client.create_bucket(Bucket='bucket{}'.format( 30 | num 31 | )) 32 | scan_result = s3_scanner_resource.gather(region='us-east-1') 33 | assert len(scan_result) == 500 34 | 35 | @mock_s3 36 | def test_s3_resource_multi_region(): 37 | 38 | s3_scanner_resource = bucket.resource() 39 | 40 | scan_result = s3_scanner_resource.gather('us-east-1') 41 | assert len(scan_result) == 0 42 | 43 | client = boto3.client('s3', region_name='us-east-1') 44 | 45 | east_bucket_name='us-east-1-bucket' 46 | west_bucket_name='us-west-1-bucket' 47 | client.create_bucket(Bucket=east_bucket_name) 48 | client.create_bucket(Bucket=west_bucket_name, CreateBucketConfiguration={'LocationConstraint':'us-west-1'}) 49 | 50 | us_east_1_scan_result = s3_scanner_resource.gather(region='us-east-1') 51 | us_west_1_scan_result = s3_scanner_resource.gather(region='us-west-1') 52 | 53 | assert len(us_east_1_scan_result) == 1 54 | assert len(us_west_1_scan_result) == 1 55 | assert east_bucket_name not in us_west_1_scan_result 56 | assert west_bucket_name not in us_east_1_scan_result -------------------------------------------------------------------------------- /tests/resources/test_AWS_Lambda_Function.py: -------------------------------------------------------------------------------- 1 | from cfn_sweeper import resources 2 | from cfn_sweeper.resources import AWS_Lambda_Function as aws_lambda 3 | import zipfile 4 | import io 5 | import pytest 6 | import boto3 7 | from botocore.exceptions import ClientError 8 | from moto import mock_lambda, mock_iam 9 | 10 | 11 | @mock_lambda 12 | def test_lambda_resoure(): 13 | lambda_scanner_resource = aws_lambda.resource() 14 | 15 | assert len(lambda_scanner_resource.gather('us-east-1')) == 0 16 | 17 | client = boto3.client('lambda', region_name='us-east-1') 18 | client.create_function( 19 | FunctionName="testFunction", 20 | Role=get_role_name(), 21 | Code ={"ZipFile": get_test_zip_file1()}, 22 | Handler="lambda_function.lambda_handler", 23 | Runtime='python2.7' 24 | ) 25 | 26 | assert len(lambda_scanner_resource.gather('us-east-1')) == 1 27 | assert len(lambda_scanner_resource.gather('us-east-2')) == 0 28 | 29 | for x in range(0,500): 30 | client.create_function( 31 | FunctionName='MyTestFunction{}'.format(str(x)), 32 | Role=get_role_name(), 33 | Code ={"ZipFile": get_test_zip_file1()}, 34 | Handler="lambda_function.lambda_handler", 35 | Runtime='python2.7' 36 | ) 37 | 38 | assert len(lambda_scanner_resource.gather('us-east-1')) == 501 39 | assert len(lambda_scanner_resource.gather('us-east-2')) == 0 40 | 41 | 42 | 43 | def get_role_name(): 44 | with mock_iam(): 45 | iam = boto3.client("iam") 46 | try: 47 | return iam.get_role(RoleName="my-role")["Role"]["Arn"] 48 | except ClientError: 49 | return iam.create_role( 50 | RoleName="my-role", 51 | AssumeRolePolicyDocument="some policy", 52 | Path="/my-path/", 53 | )["Role"]["Arn"] 54 | 55 | 56 | def _process_lambda(func_str): 57 | zip_output = io.BytesIO() 58 | zip_file = zipfile.ZipFile(zip_output, "w", zipfile.ZIP_DEFLATED) 59 | zip_file.writestr("lambda_function.py", func_str) 60 | zip_file.close() 61 | zip_output.seek(0) 62 | return zip_output.read() 63 | 64 | 65 | def get_test_zip_file1(): 66 | pfunc = """ 67 | def lambda_handler(event, context): 68 | print("custom log event") 69 | return event 70 | """ 71 | return _process_lambda(pfunc) 72 | -------------------------------------------------------------------------------- /tests/base/test_validation.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from cfn_sweeper.validation import ValidateRegion,Validateoutput,Validatefilter 3 | import pytest 4 | from contextlib import contextmanager 5 | 6 | 7 | @contextmanager 8 | def not_raises(exception): 9 | try: 10 | yield 11 | except exception: 12 | raise pytest.fail("DID RAISE {0}".format(exception)) 13 | 14 | 15 | def test_required_unknown(): 16 | """ Try to perform sweep on something that isn't an option. """ 17 | parser=argparse.ArgumentParser() 18 | parser.add_argument('--region', 19 | help='Enter a region like us-east-2.', 20 | dest="region", 21 | action=ValidateRegion, 22 | required=True) 23 | parser.add_argument('--output', 24 | help='pretty, json, yaml', 25 | dest="output", 26 | action=Validateoutput, 27 | nargs="?", 28 | default="yaml" 29 | ) 30 | parser.add_argument('--filter-types', 31 | help='eg: AWS::IAM::Role or AWS::EC2::Instance. Using "ALL" with no quotes and we will run it for all current supported resource types', 32 | nargs='+', 33 | dest="types", 34 | action=Validatefilter, 35 | required=True) 36 | parser.add_argument('--tag_keys', 37 | help='Allows you to exclude particular AWS Resources based on the presence of a particular tag key on the resource. This will only be applied to AWS Resources that support tagging. Valid values: any string that is a valid tag - multiple values can be supplied.', 38 | dest="tags") 39 | 40 | #This should raise an error since this will cause a SystemExit since bad params were passed in 41 | args = ["--region", "NADA",'--output', "NADA",'--filter-types',"NADA"] 42 | with pytest.raises(SystemExit): 43 | parser.parse_args(args) 44 | 45 | 46 | 47 | 48 | #This should NOT raise an error since good params were passed into the parser 49 | args = ["--region", "us-east-1",'--output', "yaml",'--filter-types',"AWS::EC2::Instance"] 50 | with not_raises(SystemExit): 51 | parser.parse_args(args) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # VSCode 75 | .vscode/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # operating system-related files 135 | # file properties cache/storage on macOS 136 | *.DS_Store 137 | # thumbnail cache on Windows 138 | Thumbs.db 139 | 140 | # profiling data 141 | .prof 142 | 143 | # pytype static type analyzer 144 | .pytype/ 145 | 146 | #https://www.toptal.com/developers/gitignore Generated Here<-- -------------------------------------------------------------------------------- /cfn_sweeper/base/runner.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | from pkgutil import walk_packages 3 | import os 4 | 5 | def get_aws_modules(files:list) -> list: 6 | ''' 7 | Given a list of files, returns a list of the files that are 8 | prefixed with the AWS_ prefix 9 | 10 | This is used to filter out files that are required in the directory, 11 | such as __init__py or the abstract class 12 | 13 | Parameters: 14 | files (list): an Array of strings - file names 15 | ''' 16 | 17 | return list(filter(lambda x: x.startswith('AWS_'), files)) 18 | 19 | def get_module_dir() -> str: 20 | ''' 21 | Returns the absolute directory of the resources folder - where the modules 22 | for AWS resources are located 23 | ''' 24 | root_dir = os.path.dirname(os.path.abspath(__file__)) 25 | root_dir_len = len(root_dir) 26 | return root_dir[:root_dir_len - 4] + 'resources' 27 | 28 | def get_package_modules(package_path:str) -> list: 29 | result = [] 30 | for package in walk_packages([package_path]): 31 | result.append(package.name) 32 | 33 | return result 34 | 35 | 36 | class PluginManager: 37 | ''' 38 | This class is used to manage and interact with the various AWS resource modules 39 | stored in the cfn_sweeper/resources section of the project. Upon initialization - 40 | the PluginManager class will dynamically load each module, and load it into a 41 | dict object with a key of the resource_name (eg - AWS::S3::Bucket or 42 | AWS::EC2::Instance), and the modules corresponding gather() function as the value. 43 | The dict shouldn't be directly interacted with - instead this class exposes a 44 | gather_resource() method that expects a cloudformation resource_name, and aws region. 45 | The method will then look for a matching key in the dict - and execute its associated 46 | gather() method with the provided region parameter. 47 | ''' 48 | 49 | def __init__(self): 50 | self.modules = {} 51 | module_dir_files = get_package_modules(get_module_dir()) 52 | aws_modules = get_aws_modules(module_dir_files) 53 | for module in aws_modules: 54 | 55 | module_path = 'cfn_sweeper.resources.{}'.format(module.split('.')[0]) 56 | try: 57 | loaded_module = import_module(module_path) 58 | resource = loaded_module.resource() 59 | self.modules[resource.resource_name] = resource.gather 60 | 61 | except ModuleNotFoundError as e: 62 | print(e) 63 | print('unable to load {}'.format(module)) 64 | 65 | def gather_resource(self, region:str, resource_name:str): 66 | try: 67 | result = self.modules[resource_name](region=region) 68 | return result 69 | except KeyError as e: 70 | print(e) 71 | raise NotImplementedError -------------------------------------------------------------------------------- /cfn_sweeper/base/cfn_resources.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | from botocore.config import Config 3 | 4 | cfn_config = Config( 5 | retries={ 6 | 'max_attempts': 10, 7 | 'mode': 'adaptive' 8 | } 9 | ) 10 | 11 | 12 | def load_cfn_resources(region: str) -> list: 13 | """ 14 | Gets all the Cloudformation managed resources for the given AWS region 15 | 16 | Parameters: 17 | region (string): the AWS Region to scan 18 | 19 | Returns: 20 | An array of dict - containing the information of Cloudformation resources 21 | 22 | """ 23 | cloudformation_client = boto3.client( 24 | service_name='cloudformation', region_name=region, config=cfn_config) 25 | stacks_in_account = [] 26 | stack_paginator = cloudformation_client.get_paginator('list_stacks') 27 | 28 | stack_page_iterator = stack_paginator.paginate( 29 | StackStatusFilter=['CREATE_COMPLETE', 'UPDATE_COMPLETE']) 30 | for page in stack_page_iterator: 31 | stacks_in_account.extend(page['StackSummaries']) 32 | resource_paginator = cloudformation_client.get_paginator( 33 | 'list_stack_resources') 34 | result = [] 35 | for stack in stacks_in_account: 36 | resource_page_iterator = resource_paginator.paginate( 37 | StackName=stack['StackName']) 38 | for page in resource_page_iterator: 39 | for resource in page['StackResourceSummaries']: 40 | result.append(resource) 41 | return result 42 | 43 | 44 | def get_all_cfn_resources_by_type(resource_array: list, resource_type: str) -> list: 45 | """ 46 | Given a list of cloudformation stack resources, filters the resources by the specified type 47 | 48 | Parameters: 49 | resource_array (list): an Array of Cloudformation Stack Resources 50 | resource_type (string): the Name of the Cloudformation Resource type to filter for - example: AWS::EC2::Instance 51 | 52 | Returns: 53 | An array of dict - containing the filtered Cloudformation resources 54 | """ 55 | result = [] 56 | 57 | for resource in resource_array: 58 | if resource['ResourceType'] == resource_type: 59 | result.append(resource) 60 | return result 61 | 62 | 63 | def is_managed_by_cloudformation(physical_resource_id: str, resource_array: list) -> bool: 64 | """ 65 | Given a physical resource id and array of rources - returns if the resource is managed by cloudformation 66 | 67 | Parameters: 68 | physical_resource_id (string): The identifier of the resource - eg igw-09a7b4932e331edb2 or vpc-054a63a50efe678b3 69 | resource_array (list): an array of cloudformation stack resources 70 | 71 | Returns: 72 | boolean - if the resource is found in the list of resource_array 73 | """ 74 | 75 | for resource in resource_array: 76 | if resource['PhysicalResourceId'] == physical_resource_id: 77 | return True 78 | else: 79 | return False 80 | -------------------------------------------------------------------------------- /tests/base/test_runner.py: -------------------------------------------------------------------------------- 1 | from cfn_sweeper.base.runner import PluginManager, get_aws_modules, get_module_dir, get_package_modules 2 | import uuid 3 | import pytest 4 | 5 | def test_get_aws_modules(): 6 | list_dir = [ 7 | 'AWS_EC2_Instance.py', 8 | 'AWS_IAM_Role.py', 9 | 'NOT_AN_AWS_MODULE', 10 | '__init__.py', 11 | 'base.py' 12 | ] 13 | 14 | result = get_aws_modules(list_dir) 15 | assert '__init__.py' not in result 16 | assert 'base.py' not in result 17 | assert 'NOT_AN_AWS_MODULE' not in result 18 | assert len(result) == 2 19 | assert 'AWS_EC2_Instance.py' in result 20 | assert 'AWS_IAM_Role.py' in result 21 | 22 | def test_get_module_dir(mocker): 23 | mocker.patch( 24 | 'cfn_sweeper.base.runner.os.path.dirname', 25 | return_value='/home/randomuser/cfn-sweeper/cfn-sweeper/base' 26 | ) 27 | 28 | plugin_dir = get_module_dir() 29 | assert plugin_dir == '/home/randomuser/cfn-sweeper/cfn-sweeper/resources' 30 | 31 | def test_get_package_modules(mocker): 32 | mocker.patch( 33 | 'cfn_sweeper.base.runner.walk_packages', 34 | return_value=[ 35 | FakePkgUtilModuleClass(name='module1'), 36 | FakePkgUtilModuleClass(name='module2') 37 | ] 38 | ) 39 | 40 | result = get_package_modules('module_path') 41 | assert len(result) == 2 42 | assert 'module1' in result 43 | assert 'module2' in result 44 | 45 | def test_PluginManager(mocker): 46 | mocker.patch( 47 | 'cfn_sweeper.base.runner.get_package_modules', 48 | return_value=[ 49 | 'AWS_EC2_Instance.py', 50 | 'AWS_S3_Bucket.py', 51 | '__init__.py', 52 | 'NOT_AN_AWS_MODULE.py' 53 | ] 54 | ) 55 | 56 | mocker.patch( 57 | 'cfn_sweeper.base.runner.os.path.dirname', 58 | return_value='/home/randomuser/cfn-sweeper/cfn-sweeper/base' 59 | ) 60 | 61 | mocker.patch( 62 | 'cfn_sweeper.base.runner.import_module', 63 | return_value=FakeModuleClass() 64 | ) 65 | 66 | runner = PluginManager() 67 | assert runner.gather_resource(region='us-east-1', resource_name='AWS::Resource::Thing') == [ 68 | 'aws-thing-1', 69 | 'aws-thing-2' 70 | ] 71 | 72 | with pytest.raises(NotImplementedError) as error_info: 73 | runner.gather_resource(region='us-east-1', resource_name='AWS::S3::Bucket') 74 | assert "NotImplementedError" in str(error_info) 75 | 76 | """ Sadly not sure of a better way to test this - but essentually creating fake classes to simulate 77 | how our plugins would behave when dynamically imported using import_lib 78 | """ 79 | 80 | class FakeModuleClass(): 81 | def resource(self): 82 | return FakeResourceClass() 83 | 84 | class FakeResourceClass(): 85 | def __init__(self): 86 | self.resource_name = 'AWS::Resource::Thing' 87 | 88 | def gather(self, region): 89 | return [ 90 | 'aws-thing-1', 91 | 'aws-thing-2' 92 | ] 93 | 94 | class FakePkgUtilModuleClass(): 95 | def __init__(self, name): 96 | self.name = name -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cfn-sweeper - find all the unmanaged resources in your account! 2 | [![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://github.com/PyCQA/bandit) 3 | [![GitHub license](https://img.shields.io/github/license/Naereen/StrapDown.js.svg)](https://github.com/Naereen/StrapDown.js/blob/master/LICENSE) 4 | [![made-with-python](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org/) 5 | [![Total alerts](https://img.shields.io/lgtm/alerts/g/rileydakota/cfn-sweeper.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/rileydakota/cfn-sweeper/alerts/) 6 | [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/rileydakota/cfn-sweeper.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/rileydakota/cfn-sweeper/context:python) 7 | 8 | 9 | 10 | 11 | A CLI Tool to find resources in an AWS Account not actively managed by Cloudformation! 12 | 13 | Wanting to understand how many resources in your AWS Account are managed by Cloudformation? This is the tool for you! 14 | 15 | ## Installation 16 | 17 | > :warning: **At least Python3.8 is required! 18 | 19 | Download the package from PyPI: 20 | ```pip install cfn-sweeper``` 21 | 22 | Or install directly from source: 23 | ```git clone git@github.com:rileydakota/cfn-sweeper.git && python3 cfn-sweeper/setup.py``` 24 | 25 | ## Usage 26 | 27 | ```bash 28 | cfn-sweeper --region us-east-1 --filter-types AWS::S3::Bucket AWS::EC2::Instance AWS::EFS::FileSystem 29 | AWS::EC2::Instance: 30 | managed: 31 | - i-04738a0664ab77af4 32 | unmanaged: [] 33 | AWS::S3::Bucket 34 | managed: [] 35 | unmanaged: 36 | - my-leftover-bucket 37 | AWS::EFS::FileSystem 38 | managed: 39 | - fs-123456 40 | unmanaged: 41 | - fs-789101 42 | ``` 43 | ### Available arguments 44 | 45 | `--output` 46 | 47 | Controls the output format of the results. Printed to stdout. 48 | Valid values: `pretty`, `json`, `yaml` 49 | 50 | `--region` 51 | 52 | The AWS Region from which we will run `describe-stacks` and look for the various resources in 53 | Valid values: any valid AWS region in kebab case format (eg `us-east-1`) 54 | 55 | `--filter-types` 56 | 57 | Allows you to exclude particular AWS Resource types based on the Cloudformation type (eg `AWS::IAM::Role` or `AWS::EC2::Instance`) 58 | Valid values: any Cloudformation resource type that is supported by the tool today (see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html for reference). Multiple values can be supplied 59 | 60 | 61 | `--filter-tag-keys` [WIP] 62 | 63 | Allows you to exclude particular AWS Resources based on the presence of a particular tag key on the resource. This will only be applied to AWS Resources that support tagging. 64 | Valid values: any string that is a valid tag - multiple values can be supplied 65 | 66 | ### Supported Cloudformation Types 67 | 68 | `AWS::IAM::Role` - (Global resoures are experimental at this time, use with caution) 69 | 70 | `AWS::EC2::Instance` 71 | 72 | `AWS::Lambda::Function` 73 | 74 | `AWS::S3::Bucket` 75 | 76 | `AWS::KMS::Key` 77 | 78 | `AWS::EFS::FileSystem` 79 | 80 | ### Using as a Python Module 81 | 82 | TBD 83 | 84 | ### FAQ 85 | 86 | TBD 87 | -------------------------------------------------------------------------------- /tests/base/test_cfn_resources.py: -------------------------------------------------------------------------------- 1 | from cfn_sweeper.base.cfn_resources import is_managed_by_cloudformation, get_all_cfn_resources_by_type, load_cfn_resources 2 | import pytest 3 | import boto3 4 | import json 5 | from moto import mock_cloudformation 6 | 7 | @mock_cloudformation 8 | def test_load_cfn_resources(): 9 | 10 | dummy_template1 = { 11 | "AWSTemplateFormatVersion": "2010-09-09", 12 | "Description": "Stack 3", 13 | "Resources": { 14 | "VPC": {"Properties": {"CidrBlock": "192.168.0.0/16"}, "Type": "AWS::EC2::VPC"} 15 | }, 16 | } 17 | 18 | dummy_template2 = { 19 | "AWSTemplateFormatVersion": "2010-09-09", 20 | "Description": "Stack 1", 21 | "Resources": { 22 | "EC2Instance1": { 23 | "Type": "AWS::EC2::Instance", 24 | "Properties": { 25 | "ImageId": '11111111122334324', 26 | "KeyName": "dummy", 27 | "InstanceType": "t2.micro", 28 | "Tags": [ 29 | {"Key": "Description", "Value": "Test tag"}, 30 | {"Key": "Name", "Value": "Name tag for tests"}, 31 | ], 32 | }, 33 | } 34 | }, 35 | } 36 | 37 | client = boto3.client('cloudformation', 'us-east-1') 38 | client.create_stack(StackName="test_stack1", TemplateBody=json.dumps(dummy_template1)) 39 | client.create_stack(StackName="test_stack2", TemplateBody=json.dumps(dummy_template2)) 40 | 41 | cfn_resources = load_cfn_resources('us-east-1') 42 | 43 | assert len(cfn_resources) == 2 44 | assert cfn_resources[0]['PhysicalResourceId'] 45 | assert cfn_resources[0]['ResourceType'] 46 | assert cfn_resources[0]['ResourceStatus'] 47 | 48 | client.delete_stack(StackName="test_stack1") 49 | client.delete_stack(StackName="test_stack2") 50 | 51 | cfn_resources = load_cfn_resources('us-east-1') 52 | assert len(cfn_resources) == 0 53 | 54 | def test_get_all_cfn_resources_by_type(): 55 | resource_array = [{ 56 | "ResourceType":"AWS::EC2::Instance" 57 | },{ 58 | "ResourceType":"AWS::Lambda::Function" 59 | }, 60 | { 61 | "ResourceType":"AWS::IAM::Role" 62 | }] 63 | 64 | result = get_all_cfn_resources_by_type(resource_array, 'AWS::EC2::Instance') 65 | 66 | assert type(result) is list 67 | assert len(result) == 1 68 | assert result[0]['ResourceType'] == 'AWS::EC2::Instance' 69 | 70 | resource_array = [{ 71 | "ResourceType":"AWS::Lambda::Function" 72 | },{ 73 | "ResourceType":"AWS::Lambda::Function" 74 | }, 75 | { 76 | "ResourceType":"AWS::IAM::Role" 77 | }] 78 | 79 | result = get_all_cfn_resources_by_type(resource_array, 'AWS::Lambda::Function') 80 | 81 | assert len(result) == 2 82 | assert result[0]['ResourceType'] == 'AWS::Lambda::Function' 83 | assert result[1]['ResourceType'] == 'AWS::Lambda::Function' 84 | 85 | result = get_all_cfn_resources_by_type(resource_array, 'AWS::EC2::VPC') 86 | 87 | assert len(result) == 0 88 | 89 | def test_is_managed_by_cloudformation(): 90 | resource_array = [{ 91 | "PhysicalResourceId":"vpc-1234" 92 | },{ 93 | "PhysicalResourceId":"vpc-5678" 94 | },{ 95 | "PhysicalResourceId":"vpc-7891" 96 | }] 97 | 98 | 99 | assert is_managed_by_cloudformation("vpc-1234", resource_array) == True 100 | assert is_managed_by_cloudformation("vpc-7891", resource_array) == True 101 | assert is_managed_by_cloudformation("vpc-1112", resource_array) == False -------------------------------------------------------------------------------- /cfn_sweeper/main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from cfn_sweeper.base.cfn_resources import load_cfn_resources, get_all_cfn_resources_by_type, is_managed_by_cloudformation 3 | from cfn_sweeper.base.runner import PluginManager 4 | from cfn_sweeper.base.output import ScanReport 5 | from cfn_sweeper.validation import ValidateRegion,Validateoutput,Validatefilter 6 | from sys import exit 7 | 8 | 9 | 10 | 11 | 12 | def main(): 13 | 14 | parser = argparse.ArgumentParser() 15 | parser.add_argument('--region', 16 | help='Enter a region like us-east-2.', 17 | dest="region", 18 | action=ValidateRegion) 19 | parser.add_argument('--output', 20 | help='pretty, json, yaml, stdout', 21 | dest="output", 22 | action=Validateoutput, 23 | nargs="?", 24 | default="yaml" 25 | ) 26 | parser.add_argument('--filter-types', 27 | help='eg: AWS::IAM::Role or AWS::EC2::Instance.', 28 | nargs='+', 29 | dest="types", 30 | action=Validatefilter) 31 | parser.add_argument('--tag_keys', 32 | help='Allows you to exclude particular AWS Resources based on the presence of a particular tag key on the resource. This will only be applied to AWS Resources that support tagging. Valid values: any string that is a valid tag - multiple values can be supplied.', 33 | dest="tags") 34 | parser.add_argument('--supported', 35 | help='Gives all current supported resource types for CFN Sweeper.', 36 | dest="supported", 37 | action='store_true') 38 | 39 | #TODO: add argument validation - including regex for patterns 40 | args = parser.parse_args() 41 | 42 | #TODO: inital shot at this - refactor later 43 | region = args.region 44 | types = args.types 45 | output = args.output 46 | cfn_resources = load_cfn_resources(region) 47 | runner = PluginManager() 48 | if args.supported: 49 | print('The following resource types are supported:') 50 | for key in runner.modules.keys(): 51 | print(" {}".format(key)) 52 | exit() 53 | 54 | #TODO: abstract result into its own class - so we can easily output to different formats 55 | report = ScanReport() 56 | for resource_type in types: 57 | resources_in_cloudformation = get_all_cfn_resources_by_type(cfn_resources, resource_type) 58 | try: 59 | resources_in_account = runner.gather_resource(region=region, resource_name=resource_type) 60 | except NotImplementedError: 61 | print('Sorry - {} isn''t supported just yet! Use --supported to see a list of supported types'.format( 62 | resource_type 63 | )) 64 | continue 65 | 66 | managed_resources = [] 67 | unmanaged_resources = [] 68 | for resource in resources_in_account: 69 | cfn_managed = is_managed_by_cloudformation(physical_resource_id=resource, resource_array=resources_in_cloudformation) 70 | if cfn_managed: 71 | managed_resources.append(resource) 72 | else: 73 | unmanaged_resources.append(resource) 74 | report.add_resource_results(resource_type=resource_type, managed_resources=managed_resources, unmanaged_resources=unmanaged_resources) 75 | 76 | 77 | if output == 'yaml': 78 | report.print_to_yaml() 79 | elif output == 'json': 80 | report.print_to_json() 81 | elif output == 'stdout': 82 | report.print_to_stdout() 83 | elif output == 'pretty': 84 | report.print_to_pretty() 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | if __name__ == '__main__': 102 | main() 103 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "abddd4b1ea89495aca944f3d1eb61108f694122a6ca33a1a23a1afa69d965977" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "boto3": { 20 | "hashes": [ 21 | "sha256:542336dda9a728c250cf24aea6d87454136d9d6f3d8a84ec5a737a7edba3b932", 22 | "sha256:9bf2a281a6df9f8948d3d322d532d03a1039f57a049a1aa2b72b4a28c9627013" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.18.30" 26 | }, 27 | "botocore": { 28 | "hashes": [ 29 | "sha256:26ab09126dd05c968fbbcb894a1d623355e6119ff6d4a2bf5d292e3ad7cdd628", 30 | "sha256:9b0b3dbc144178e2b803097abcc95712a03b8dde5a02e4335ac870bc6c129dd9" 31 | ], 32 | "markers": "python_version >= '3.6'", 33 | "version": "==1.21.30" 34 | }, 35 | "jmespath": { 36 | "hashes": [ 37 | "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", 38 | "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" 39 | ], 40 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 41 | "version": "==0.10.0" 42 | }, 43 | "pyfiglet": { 44 | "hashes": [ 45 | "sha256:c6c2321755d09267b438ec7b936825a4910fec696292139e664ca8670e103639", 46 | "sha256:d555bcea17fbeaf70eaefa48bb119352487e629c9b56f30f383e2c62dd67a01c" 47 | ], 48 | "index": "pypi", 49 | "version": "==0.8.post1" 50 | }, 51 | "python-dateutil": { 52 | "hashes": [ 53 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 54 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 55 | ], 56 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 57 | "version": "==2.8.2" 58 | }, 59 | "pyyaml": { 60 | "hashes": [ 61 | "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", 62 | "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", 63 | "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", 64 | "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", 65 | "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", 66 | "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", 67 | "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", 68 | "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", 69 | "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", 70 | "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", 71 | "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", 72 | "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", 73 | "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347", 74 | "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", 75 | "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541", 76 | "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", 77 | "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", 78 | "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc", 79 | "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", 80 | "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa", 81 | "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", 82 | "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122", 83 | "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", 84 | "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", 85 | "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", 86 | "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", 87 | "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", 88 | "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", 89 | "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" 90 | ], 91 | "index": "pypi", 92 | "version": "==5.4.1" 93 | }, 94 | "s3transfer": { 95 | "hashes": [ 96 | "sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c", 97 | "sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803" 98 | ], 99 | "markers": "python_version >= '3.6'", 100 | "version": "==0.5.0" 101 | }, 102 | "six": { 103 | "hashes": [ 104 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 105 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 106 | ], 107 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 108 | "version": "==1.16.0" 109 | }, 110 | "urllib3": { 111 | "hashes": [ 112 | "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", 113 | "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" 114 | ], 115 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", 116 | "version": "==1.26.6" 117 | } 118 | }, 119 | "develop": { 120 | "astroid": { 121 | "hashes": [ 122 | "sha256:b6c2d75cd7c2982d09e7d41d70213e863b3ba34d3bd4014e08f167cee966e99e", 123 | "sha256:ecc50f9b3803ebf8ea19aa2c6df5622d8a5c31456a53c741d3be044d96ff0948" 124 | ], 125 | "markers": "python_version ~= '3.6'", 126 | "version": "==2.7.2" 127 | }, 128 | "attrs": { 129 | "hashes": [ 130 | "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", 131 | "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" 132 | ], 133 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 134 | "version": "==21.2.0" 135 | }, 136 | "autopep8": { 137 | "hashes": [ 138 | "sha256:276ced7e9e3cb22e5d7c14748384a5cf5d9002257c0ed50c0e075b68011bb6d0", 139 | "sha256:aa213493c30dcdac99537249ee65b24af0b2c29f2e83cd8b3f68760441ed0db9" 140 | ], 141 | "index": "pypi", 142 | "version": "==1.5.7" 143 | }, 144 | "aws-sam-translator": { 145 | "hashes": [ 146 | "sha256:0ecadda9cf5ab2318f57f1253181a2151e4c53cd35d21717a923c075a5a65cb6", 147 | "sha256:dc6b816bb5cfd9709299f9b263fc0cf5ae60aca4166d1c90413ece651f1556bb", 148 | "sha256:ee7c7c5e44ec67202622ca877140545496527ffcc45da3beeda966f007443a88" 149 | ], 150 | "version": "==1.38.0" 151 | }, 152 | "aws-xray-sdk": { 153 | "hashes": [ 154 | "sha256:487e44a2e0b2a5b994f7db5fad3a8115f1ea238249117a119bce8ca2750661bd", 155 | "sha256:90c2fcc982a770e86d009a4c3d2b5c3e372da91cb8284d982bae458e2c0bb268" 156 | ], 157 | "version": "==2.8.0" 158 | }, 159 | "bandit": { 160 | "hashes": [ 161 | "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07", 162 | "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608" 163 | ], 164 | "index": "pypi", 165 | "version": "==1.7.0" 166 | }, 167 | "bleach": { 168 | "hashes": [ 169 | "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da", 170 | "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994" 171 | ], 172 | "markers": "python_version >= '3.6'", 173 | "version": "==4.1.0" 174 | }, 175 | "boto3": { 176 | "hashes": [ 177 | "sha256:542336dda9a728c250cf24aea6d87454136d9d6f3d8a84ec5a737a7edba3b932", 178 | "sha256:9bf2a281a6df9f8948d3d322d532d03a1039f57a049a1aa2b72b4a28c9627013" 179 | ], 180 | "index": "pypi", 181 | "version": "==1.18.30" 182 | }, 183 | "botocore": { 184 | "hashes": [ 185 | "sha256:26ab09126dd05c968fbbcb894a1d623355e6119ff6d4a2bf5d292e3ad7cdd628", 186 | "sha256:9b0b3dbc144178e2b803097abcc95712a03b8dde5a02e4335ac870bc6c129dd9" 187 | ], 188 | "markers": "python_version >= '3.6'", 189 | "version": "==1.21.30" 190 | }, 191 | "build": { 192 | "hashes": [ 193 | "sha256:32290592c8ccf70ce84107962f6129407abf52cedaa752af28c0c95d99dfa2e7", 194 | "sha256:d8d8417caff47888274d677f984de509554637dd1ea952d467b027849b06d83b" 195 | ], 196 | "index": "pypi", 197 | "version": "==0.6.0.post1" 198 | }, 199 | "certifi": { 200 | "hashes": [ 201 | "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", 202 | "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" 203 | ], 204 | "version": "==2021.5.30" 205 | }, 206 | "cffi": { 207 | "hashes": [ 208 | "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d", 209 | "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771", 210 | "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872", 211 | "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c", 212 | "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc", 213 | "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762", 214 | "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202", 215 | "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5", 216 | "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548", 217 | "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a", 218 | "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f", 219 | "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20", 220 | "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218", 221 | "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c", 222 | "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e", 223 | "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56", 224 | "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224", 225 | "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a", 226 | "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2", 227 | "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a", 228 | "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819", 229 | "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346", 230 | "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b", 231 | "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e", 232 | "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534", 233 | "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb", 234 | "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0", 235 | "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156", 236 | "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd", 237 | "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87", 238 | "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc", 239 | "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195", 240 | "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33", 241 | "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f", 242 | "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d", 243 | "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd", 244 | "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728", 245 | "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7", 246 | "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca", 247 | "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99", 248 | "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf", 249 | "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e", 250 | "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c", 251 | "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5", 252 | "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69" 253 | ], 254 | "version": "==1.14.6" 255 | }, 256 | "cfn-lint": { 257 | "hashes": [ 258 | "sha256:b7f5964842f7a44c5af9c61d64308dc4bcb718cf5de5428781d5564e9663463d", 259 | "sha256:d17359e3ca9477eccaea700fac4bf028f5bc368a338c017adde5187f2691cab8" 260 | ], 261 | "version": "==0.53.0" 262 | }, 263 | "charset-normalizer": { 264 | "hashes": [ 265 | "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b", 266 | "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3" 267 | ], 268 | "markers": "python_version >= '3'", 269 | "version": "==2.0.4" 270 | }, 271 | "colorama": { 272 | "hashes": [ 273 | "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", 274 | "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" 275 | ], 276 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 277 | "version": "==0.4.4" 278 | }, 279 | "cryptography": { 280 | "hashes": [ 281 | "sha256:0a7dcbcd3f1913f664aca35d47c1331fce738d44ec34b7be8b9d332151b0b01e", 282 | "sha256:1eb7bb0df6f6f583dd8e054689def236255161ebbcf62b226454ab9ec663746b", 283 | "sha256:21ca464b3a4b8d8e86ba0ee5045e103a1fcfac3b39319727bc0fc58c09c6aff7", 284 | "sha256:34dae04a0dce5730d8eb7894eab617d8a70d0c97da76b905de9efb7128ad7085", 285 | "sha256:3520667fda779eb788ea00080124875be18f2d8f0848ec00733c0ec3bb8219fc", 286 | "sha256:3fa3a7ccf96e826affdf1a0a9432be74dc73423125c8f96a909e3835a5ef194a", 287 | "sha256:5b0fbfae7ff7febdb74b574055c7466da334a5371f253732d7e2e7525d570498", 288 | "sha256:8695456444f277af73a4877db9fc979849cd3ee74c198d04fc0776ebc3db52b9", 289 | "sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c", 290 | "sha256:94fff993ee9bc1b2440d3b7243d488c6a3d9724cc2b09cdb297f6a886d040ef7", 291 | "sha256:9965c46c674ba8cc572bc09a03f4c649292ee73e1b683adb1ce81e82e9a6a0fb", 292 | "sha256:a00cf305f07b26c351d8d4e1af84ad7501eca8a342dedf24a7acb0e7b7406e14", 293 | "sha256:a305600e7a6b7b855cd798e00278161b681ad6e9b7eca94c721d5f588ab212af", 294 | "sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e", 295 | "sha256:d2a6e5ef66503da51d2110edf6c403dc6b494cc0082f85db12f54e9c5d4c3ec5", 296 | "sha256:d9ec0e67a14f9d1d48dd87a2531009a9b251c02ea42851c060b25c782516ff06", 297 | "sha256:f44d141b8c4ea5eb4dbc9b3ad992d45580c1d22bf5e24363f2fbf50c2d7ae8a7" 298 | ], 299 | "markers": "python_version >= '3.6'", 300 | "version": "==3.4.8" 301 | }, 302 | "docker": { 303 | "hashes": [ 304 | "sha256:3e8bc47534e0ca9331d72c32f2881bb13b93ded0bcdeab3c833fb7cf61c0a9a5", 305 | "sha256:fc961d622160e8021c10d1bcabc388c57d55fb1f917175afbe24af442e6879bd" 306 | ], 307 | "version": "==5.0.0" 308 | }, 309 | "docopt": { 310 | "hashes": [ 311 | "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" 312 | ], 313 | "version": "==0.6.2" 314 | }, 315 | "docutils": { 316 | "hashes": [ 317 | "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125", 318 | "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61" 319 | ], 320 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 321 | "version": "==0.17.1" 322 | }, 323 | "ecdsa": { 324 | "hashes": [ 325 | "sha256:64c613005f13efec6541bb0a33290d0d03c27abab5f15fbab20fb0ee162bdd8e", 326 | "sha256:e108a5fe92c67639abae3260e43561af914e7fd0d27bae6d2ec1312ae7934dfe" 327 | ], 328 | "version": "==0.14.1" 329 | }, 330 | "flake8": { 331 | "hashes": [ 332 | "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b", 333 | "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907" 334 | ], 335 | "index": "pypi", 336 | "version": "==3.9.2" 337 | }, 338 | "future": { 339 | "hashes": [ 340 | "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" 341 | ], 342 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 343 | "version": "==0.18.2" 344 | }, 345 | "gitdb": { 346 | "hashes": [ 347 | "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0", 348 | "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005" 349 | ], 350 | "markers": "python_version >= '3.4'", 351 | "version": "==4.0.7" 352 | }, 353 | "gitpython": { 354 | "hashes": [ 355 | "sha256:b838a895977b45ab6f0cc926a9045c8d1c44e2b653c1fcc39fe91f42c6e8f05b", 356 | "sha256:fce760879cd2aebd2991b3542876dc5c4a909b30c9d69dfc488e504a8db37ee8" 357 | ], 358 | "markers": "python_version >= '3.6'", 359 | "version": "==3.1.18" 360 | }, 361 | "idna": { 362 | "hashes": [ 363 | "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", 364 | "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" 365 | ], 366 | "markers": "python_version >= '3'", 367 | "version": "==2.10" 368 | }, 369 | "importlib-metadata": { 370 | "hashes": [ 371 | "sha256:9e04bf59076a15a9b6dd9c27806e8fcdf15280ba529c6a8cc3f4d5b4875bdd61", 372 | "sha256:c4eb3dec5f697682e383a39701a7de11cd5c02daf8dd93534b69e3e6473f6b1b" 373 | ], 374 | "markers": "python_version >= '3.6'", 375 | "version": "==4.7.1" 376 | }, 377 | "iniconfig": { 378 | "hashes": [ 379 | "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", 380 | "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" 381 | ], 382 | "version": "==1.1.1" 383 | }, 384 | "isort": { 385 | "hashes": [ 386 | "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899", 387 | "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2" 388 | ], 389 | "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", 390 | "version": "==5.9.3" 391 | }, 392 | "jeepney": { 393 | "hashes": [ 394 | "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac", 395 | "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f" 396 | ], 397 | "index": "pypi", 398 | "version": "==0.7.1" 399 | }, 400 | "jinja2": { 401 | "hashes": [ 402 | "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", 403 | "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" 404 | ], 405 | "markers": "python_version >= '3.6'", 406 | "version": "==3.0.1" 407 | }, 408 | "jmespath": { 409 | "hashes": [ 410 | "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", 411 | "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" 412 | ], 413 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 414 | "version": "==0.10.0" 415 | }, 416 | "jsondiff": { 417 | "hashes": [ 418 | "sha256:5122bf4708a031b02db029366184a87c5d0ddd5a327a5884ee6cf0193e599d71" 419 | ], 420 | "version": "==1.3.0" 421 | }, 422 | "jsonpatch": { 423 | "hashes": [ 424 | "sha256:26ac385719ac9f54df8a2f0827bb8253aa3ea8ab7b3368457bcdb8c14595a397", 425 | "sha256:b6ddfe6c3db30d81a96aaeceb6baf916094ffa23d7dd5fa2c13e13f8b6e600c2" 426 | ], 427 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 428 | "version": "==1.32" 429 | }, 430 | "jsonpointer": { 431 | "hashes": [ 432 | "sha256:150f80c5badd02c757da6644852f612f88e8b4bc2f9852dcbf557c8738919686", 433 | "sha256:5a34b698db1eb79ceac454159d3f7c12a451a91f6334a4f638454327b7a89962" 434 | ], 435 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 436 | "version": "==2.1" 437 | }, 438 | "jsonschema": { 439 | "hashes": [ 440 | "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163", 441 | "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a" 442 | ], 443 | "version": "==3.2.0" 444 | }, 445 | "junit-xml": { 446 | "hashes": [ 447 | "sha256:ec5ca1a55aefdd76d28fcc0b135251d156c7106fa979686a4b48d62b761b4732" 448 | ], 449 | "version": "==1.9" 450 | }, 451 | "keyring": { 452 | "hashes": [ 453 | "sha256:b32397fd7e7063f8dd74a26db910c9862fc2109285fa16e3b5208bcb42a3e579", 454 | "sha256:b7e0156667f5dcc73c1f63a518005cd18a4eb23fe77321194fefcc03748b21a4" 455 | ], 456 | "markers": "python_version >= '3.6'", 457 | "version": "==23.1.0" 458 | }, 459 | "lazy-object-proxy": { 460 | "hashes": [ 461 | "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653", 462 | "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61", 463 | "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2", 464 | "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837", 465 | "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3", 466 | "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43", 467 | "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726", 468 | "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3", 469 | "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587", 470 | "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8", 471 | "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a", 472 | "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd", 473 | "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f", 474 | "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad", 475 | "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4", 476 | "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b", 477 | "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf", 478 | "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981", 479 | "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741", 480 | "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e", 481 | "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93", 482 | "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b" 483 | ], 484 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 485 | "version": "==1.6.0" 486 | }, 487 | "markupsafe": { 488 | "hashes": [ 489 | "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", 490 | "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", 491 | "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", 492 | "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", 493 | "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", 494 | "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", 495 | "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", 496 | "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", 497 | "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", 498 | "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", 499 | "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", 500 | "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", 501 | "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", 502 | "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", 503 | "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", 504 | "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", 505 | "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", 506 | "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", 507 | "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", 508 | "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", 509 | "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", 510 | "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", 511 | "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", 512 | "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", 513 | "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", 514 | "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", 515 | "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", 516 | "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", 517 | "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", 518 | "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", 519 | "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", 520 | "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", 521 | "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", 522 | "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", 523 | "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", 524 | "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", 525 | "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", 526 | "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", 527 | "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", 528 | "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", 529 | "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", 530 | "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", 531 | "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", 532 | "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", 533 | "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", 534 | "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", 535 | "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", 536 | "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", 537 | "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", 538 | "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", 539 | "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", 540 | "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", 541 | "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", 542 | "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" 543 | ], 544 | "markers": "python_version >= '3.6'", 545 | "version": "==2.0.1" 546 | }, 547 | "mccabe": { 548 | "hashes": [ 549 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 550 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 551 | ], 552 | "version": "==0.6.1" 553 | }, 554 | "more-itertools": { 555 | "hashes": [ 556 | "sha256:2cf89ec599962f2ddc4d568a05defc40e0a587fbc10d5989713638864c36be4d", 557 | "sha256:83f0308e05477c68f56ea3a888172c78ed5d5b3c282addb67508e7ba6c8f813a" 558 | ], 559 | "markers": "python_version >= '3.5'", 560 | "version": "==8.8.0" 561 | }, 562 | "moto": { 563 | "extras": [ 564 | "all" 565 | ], 566 | "hashes": [ 567 | "sha256:c99197ee61a2aad85613814fd52661873316987794f1bbc2ff433eb6fcc684b5", 568 | "sha256:ee652be8c3bb52c6c54bcc811f74afbfb317c59dcdfdc96d6027fe93e1b4a94d" 569 | ], 570 | "index": "pypi", 571 | "version": "==2.2.4" 572 | }, 573 | "networkx": { 574 | "hashes": [ 575 | "sha256:2306f1950ce772c5a59a57f5486d59bb9cab98497c45fc49cbc45ac0dec119bb", 576 | "sha256:5fcb7004be69e8fbdf07dcb502efa5c77cadcaad6982164134eeb9721f826c2e" 577 | ], 578 | "markers": "python_version >= '3.5'", 579 | "version": "==2.6.2" 580 | }, 581 | "packaging": { 582 | "hashes": [ 583 | "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", 584 | "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" 585 | ], 586 | "markers": "python_version >= '3.6'", 587 | "version": "==21.0" 588 | }, 589 | "pbr": { 590 | "hashes": [ 591 | "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd", 592 | "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4" 593 | ], 594 | "markers": "python_version >= '2.6'", 595 | "version": "==5.6.0" 596 | }, 597 | "pep517": { 598 | "hashes": [ 599 | "sha256:3fa6b85b9def7ba4de99fb7f96fe3f02e2d630df8aa2720a5cf3b183f087a738", 600 | "sha256:e1ba5dffa3a131387979a68ff3e391ac7d645be409216b961bc2efe6468ab0b2" 601 | ], 602 | "version": "==0.11.0" 603 | }, 604 | "pkginfo": { 605 | "hashes": [ 606 | "sha256:37ecd857b47e5f55949c41ed061eb51a0bee97a87c969219d144c0e023982779", 607 | "sha256:e7432f81d08adec7297633191bbf0bd47faf13cd8724c3a13250e51d542635bd" 608 | ], 609 | "version": "==1.7.1" 610 | }, 611 | "platformdirs": { 612 | "hashes": [ 613 | "sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c", 614 | "sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e" 615 | ], 616 | "markers": "python_version >= '3.6'", 617 | "version": "==2.2.0" 618 | }, 619 | "pluggy": { 620 | "hashes": [ 621 | "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", 622 | "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" 623 | ], 624 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 625 | "version": "==0.13.1" 626 | }, 627 | "py": { 628 | "hashes": [ 629 | "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", 630 | "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" 631 | ], 632 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 633 | "version": "==1.10.0" 634 | }, 635 | "pyasn1": { 636 | "hashes": [ 637 | "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", 638 | "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", 639 | "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", 640 | "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", 641 | "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", 642 | "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", 643 | "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", 644 | "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", 645 | "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", 646 | "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", 647 | "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", 648 | "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", 649 | "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" 650 | ], 651 | "version": "==0.4.8" 652 | }, 653 | "pycodestyle": { 654 | "hashes": [ 655 | "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", 656 | "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" 657 | ], 658 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 659 | "version": "==2.7.0" 660 | }, 661 | "pycparser": { 662 | "hashes": [ 663 | "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", 664 | "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" 665 | ], 666 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 667 | "version": "==2.20" 668 | }, 669 | "pyflakes": { 670 | "hashes": [ 671 | "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", 672 | "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" 673 | ], 674 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 675 | "version": "==2.3.1" 676 | }, 677 | "pygments": { 678 | "hashes": [ 679 | "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", 680 | "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" 681 | ], 682 | "markers": "python_version >= '3.5'", 683 | "version": "==2.10.0" 684 | }, 685 | "pylint": { 686 | "hashes": [ 687 | "sha256:6758cce3ddbab60c52b57dcc07f0c5d779e5daf0cf50f6faacbef1d3ea62d2a1", 688 | "sha256:e178e96b6ba171f8ef51fbce9ca30931e6acbea4a155074d80cc081596c9e852" 689 | ], 690 | "index": "pypi", 691 | "version": "==2.10.2" 692 | }, 693 | "pyparsing": { 694 | "hashes": [ 695 | "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", 696 | "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" 697 | ], 698 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 699 | "version": "==2.4.7" 700 | }, 701 | "pyrsistent": { 702 | "hashes": [ 703 | "sha256:097b96f129dd36a8c9e33594e7ebb151b1515eb52cceb08474c10a5479e799f2", 704 | "sha256:2aaf19dc8ce517a8653746d98e962ef480ff34b6bc563fc067be6401ffb457c7", 705 | "sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea", 706 | "sha256:48578680353f41dca1ca3dc48629fb77dfc745128b56fc01096b2530c13fd426", 707 | "sha256:4916c10896721e472ee12c95cdc2891ce5890898d2f9907b1b4ae0f53588b710", 708 | "sha256:527be2bfa8dc80f6f8ddd65242ba476a6c4fb4e3aedbf281dfbac1b1ed4165b1", 709 | "sha256:58a70d93fb79dc585b21f9d72487b929a6fe58da0754fa4cb9f279bb92369396", 710 | "sha256:5e4395bbf841693eaebaa5bb5c8f5cdbb1d139e07c975c682ec4e4f8126e03d2", 711 | "sha256:6b5eed00e597b5b5773b4ca30bd48a5774ef1e96f2a45d105db5b4ebb4bca680", 712 | "sha256:73ff61b1411e3fb0ba144b8f08d6749749775fe89688093e1efef9839d2dcc35", 713 | "sha256:772e94c2c6864f2cd2ffbe58bb3bdefbe2a32afa0acb1a77e472aac831f83427", 714 | "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b", 715 | "sha256:a0c772d791c38bbc77be659af29bb14c38ced151433592e326361610250c605b", 716 | "sha256:b29b869cf58412ca5738d23691e96d8aff535e17390128a1a52717c9a109da4f", 717 | "sha256:c1a9ff320fa699337e05edcaae79ef8c2880b52720bc031b219e5b5008ebbdef", 718 | "sha256:cd3caef37a415fd0dae6148a1b6957a8c5f275a62cca02e18474608cb263640c", 719 | "sha256:d5ec194c9c573aafaceebf05fc400656722793dac57f254cd4741f3c27ae57b4", 720 | "sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d", 721 | "sha256:e79d94ca58fcafef6395f6352383fa1a76922268fa02caa2272fff501c2fdc78", 722 | "sha256:f3ef98d7b76da5eb19c37fda834d50262ff9167c65658d1d8f974d2e4d90676b", 723 | "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72" 724 | ], 725 | "markers": "python_version >= '3.6'", 726 | "version": "==0.18.0" 727 | }, 728 | "pytest": { 729 | "hashes": [ 730 | "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b", 731 | "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890" 732 | ], 733 | "index": "pypi", 734 | "version": "==6.2.4" 735 | }, 736 | "pytest-mock": { 737 | "hashes": [ 738 | "sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3", 739 | "sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62" 740 | ], 741 | "index": "pypi", 742 | "version": "==3.6.1" 743 | }, 744 | "pytest-watch": { 745 | "hashes": [ 746 | "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9" 747 | ], 748 | "index": "pypi", 749 | "version": "==4.2.0" 750 | }, 751 | "python-dateutil": { 752 | "hashes": [ 753 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 754 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 755 | ], 756 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 757 | "version": "==2.8.2" 758 | }, 759 | "python-jose": { 760 | "extras": [ 761 | "cryptography" 762 | ], 763 | "hashes": [ 764 | "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a", 765 | "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a" 766 | ], 767 | "version": "==3.3.0" 768 | }, 769 | "pytz": { 770 | "hashes": [ 771 | "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", 772 | "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" 773 | ], 774 | "version": "==2021.1" 775 | }, 776 | "pyyaml": { 777 | "hashes": [ 778 | "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", 779 | "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", 780 | "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", 781 | "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", 782 | "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", 783 | "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", 784 | "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", 785 | "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", 786 | "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", 787 | "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", 788 | "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", 789 | "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", 790 | "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347", 791 | "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", 792 | "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541", 793 | "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", 794 | "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", 795 | "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc", 796 | "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", 797 | "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa", 798 | "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", 799 | "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122", 800 | "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", 801 | "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", 802 | "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", 803 | "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", 804 | "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", 805 | "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", 806 | "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" 807 | ], 808 | "index": "pypi", 809 | "version": "==5.4.1" 810 | }, 811 | "readme-renderer": { 812 | "hashes": [ 813 | "sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c", 814 | "sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db" 815 | ], 816 | "version": "==29.0" 817 | }, 818 | "requests": { 819 | "hashes": [ 820 | "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", 821 | "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" 822 | ], 823 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 824 | "version": "==2.26.0" 825 | }, 826 | "requests-toolbelt": { 827 | "hashes": [ 828 | "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", 829 | "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" 830 | ], 831 | "version": "==0.9.1" 832 | }, 833 | "responses": { 834 | "hashes": [ 835 | "sha256:9476775d856d3c24ae660bbebe29fb6d789d4ad16acd723efbfb6ee20990b899", 836 | "sha256:d8d0f655710c46fd3513b9202a7f0dcedd02ca0f8cf4976f27fa8ab5b81e656d" 837 | ], 838 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 839 | "version": "==0.13.4" 840 | }, 841 | "rfc3986": { 842 | "hashes": [ 843 | "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835", 844 | "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97" 845 | ], 846 | "version": "==1.5.0" 847 | }, 848 | "rsa": { 849 | "hashes": [ 850 | "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2", 851 | "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9" 852 | ], 853 | "markers": "python_version >= '3.5' and python_version < '4.0'", 854 | "version": "==4.7.2" 855 | }, 856 | "s3transfer": { 857 | "hashes": [ 858 | "sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c", 859 | "sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803" 860 | ], 861 | "markers": "python_version >= '3.6'", 862 | "version": "==0.5.0" 863 | }, 864 | "secretstorage": { 865 | "hashes": [ 866 | "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f", 867 | "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195" 868 | ], 869 | "markers": "sys_platform == 'linux'", 870 | "version": "==3.3.1" 871 | }, 872 | "six": { 873 | "hashes": [ 874 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 875 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 876 | ], 877 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 878 | "version": "==1.16.0" 879 | }, 880 | "smmap": { 881 | "hashes": [ 882 | "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182", 883 | "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2" 884 | ], 885 | "markers": "python_version >= '3.5'", 886 | "version": "==4.0.0" 887 | }, 888 | "sshpubkeys": { 889 | "hashes": [ 890 | "sha256:3020ed4f8c846849299370fbe98ff4157b0ccc1accec105e07cfa9ae4bb55064", 891 | "sha256:946f76b8fe86704b0e7c56a00d80294e39bc2305999844f079a217885060b1ac" 892 | ], 893 | "version": "==3.3.1" 894 | }, 895 | "stevedore": { 896 | "hashes": [ 897 | "sha256:59b58edb7f57b11897f150475e7bc0c39c5381f0b8e3fa9f5c20ce6c89ec4aa1", 898 | "sha256:920ce6259f0b2498aaa4545989536a27e4e4607b8318802d7ddc3a533d3d069e" 899 | ], 900 | "markers": "python_version >= '3.6'", 901 | "version": "==3.4.0" 902 | }, 903 | "toml": { 904 | "hashes": [ 905 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", 906 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 907 | ], 908 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 909 | "version": "==0.10.2" 910 | }, 911 | "tomli": { 912 | "hashes": [ 913 | "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f", 914 | "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442" 915 | ], 916 | "markers": "python_version >= '3.6'", 917 | "version": "==1.2.1" 918 | }, 919 | "tqdm": { 920 | "hashes": [ 921 | "sha256:80aead664e6c1672c4ae20dc50e1cdc5e20eeff9b14aa23ecd426375b28be588", 922 | "sha256:a4d6d112e507ef98513ac119ead1159d286deab17dffedd96921412c2d236ff5" 923 | ], 924 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 925 | "version": "==4.62.2" 926 | }, 927 | "twine": { 928 | "hashes": [ 929 | "sha256:087328e9bb405e7ce18527a2dca4042a84c7918658f951110b38bc135acab218", 930 | "sha256:4caec0f1ed78dc4c9b83ad537e453d03ce485725f2aea57f1bb3fdde78dae936" 931 | ], 932 | "index": "pypi", 933 | "version": "==3.4.2" 934 | }, 935 | "urllib3": { 936 | "hashes": [ 937 | "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", 938 | "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" 939 | ], 940 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", 941 | "version": "==1.26.6" 942 | }, 943 | "watchdog": { 944 | "hashes": [ 945 | "sha256:28777dbed3bbd95f9c70f461443990a36c07dbf49ae7cd69932cdd1b8fb2850c", 946 | "sha256:41d44ef21a77a32b55ce9bf59b75777063751f688de51098859b7c7f6466589a", 947 | "sha256:43bf728eb7830559f329864ab5da2302c15b2efbac24ad84ccc09949ba753c40", 948 | "sha256:50a7f81f99d238f72185f481b493f9de80096e046935b60ea78e1276f3d76960", 949 | "sha256:51af09ae937ada0e9a10cc16988ec03c649754a91526170b6839b89fc56d6acb", 950 | "sha256:5563b005907613430ef3d4aaac9c78600dd5704e84764cb6deda4b3d72807f09", 951 | "sha256:58ae842300cbfe5e62fb068c83901abe76e4f413234b7bec5446e4275eb1f9cb", 952 | "sha256:59767f476cd1f48531bf378f0300565d879688c82da8369ca8c52f633299523c", 953 | "sha256:5cf78f794c9d7bc64a626ef4f71aff88f57a7ae288e0b359a9c6ea711a41395f", 954 | "sha256:5f57ce4f7e498278fb2a091f39359930144a0f2f90ea8cbf4523c4e25de34028", 955 | "sha256:6f3ad1d973fe8fc8fe64ba38f6a934b74346342fa98ef08ad5da361a05d46044", 956 | "sha256:78b1514067ff4089f4dac930b043a142997a5b98553120919005e97fbaba6546", 957 | "sha256:814d396859c95598f7576d15bc257c3bd3ba61fa4bc1db7dfc18f09070ded7da", 958 | "sha256:8874d5ad6b7f43b18935d9b0183e29727a623a216693d6938d07dfd411ba462f", 959 | "sha256:8b74d0d92a69a7ab5f101f9fe74e44ba017be269efa824337366ccbb4effde85", 960 | "sha256:9391003635aa783957b9b11175d9802d3272ed67e69ef2e3394c0b6d9d24fa9a", 961 | "sha256:a2888a788893c4ef7e562861ec5433875b7915f930a5a7ed3d32c048158f1be5", 962 | "sha256:a7053d4d22dc95c5e0c90aeeae1e4ed5269d2f04001798eec43a654a03008d22", 963 | "sha256:b0cc7d8b7d60da6c313779d85903ce39a63d89d866014b085f720a083d5f3e9a", 964 | "sha256:e40e33a4889382824846b4baa05634e1365b47c6fa40071dc2d06b4d7c715fc1", 965 | "sha256:e60d3bb7166b7cb830b86938d1eb0e6cfe23dfd634cce05c128f8f9967895193", 966 | "sha256:eab14adfc417c2c983fbcb2c73ef3f28ba6990d1fff45d1180bf7e38bda0d98d", 967 | "sha256:ed4ca4351cd2bb0d863ee737a2011ca44d8d8be19b43509bd4507f8a449b376b" 968 | ], 969 | "markers": "python_version >= '3.6'", 970 | "version": "==2.1.5" 971 | }, 972 | "webencodings": { 973 | "hashes": [ 974 | "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", 975 | "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" 976 | ], 977 | "version": "==0.5.1" 978 | }, 979 | "websocket-client": { 980 | "hashes": [ 981 | "sha256:0133d2f784858e59959ce82ddac316634229da55b498aac311f1620567a710ec", 982 | "sha256:8dfb715d8a992f5712fff8c843adae94e22b22a99b2c5e6b0ec4a1a981cc4e0d" 983 | ], 984 | "markers": "python_version >= '3.6'", 985 | "version": "==1.2.1" 986 | }, 987 | "werkzeug": { 988 | "hashes": [ 989 | "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42", 990 | "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8" 991 | ], 992 | "markers": "python_version >= '3.6'", 993 | "version": "==2.0.1" 994 | }, 995 | "wrapt": { 996 | "hashes": [ 997 | "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" 998 | ], 999 | "version": "==1.12.1" 1000 | }, 1001 | "xmltodict": { 1002 | "hashes": [ 1003 | "sha256:50d8c638ed7ecb88d90561beedbf720c9b4e851a9fa6c47ebd64e99d166d8a21", 1004 | "sha256:8bbcb45cc982f48b2ca8fe7e7827c5d792f217ecf1792626f808bf41c3b86051" 1005 | ], 1006 | "version": "==0.12.0" 1007 | }, 1008 | "zipp": { 1009 | "hashes": [ 1010 | "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3", 1011 | "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4" 1012 | ], 1013 | "markers": "python_version >= '3.6'", 1014 | "version": "==3.5.0" 1015 | } 1016 | } 1017 | } 1018 | --------------------------------------------------------------------------------