├── requirements.txt ├── LICENSE ├── README.md ├── .gitignore ├── setup.py └── cloud_ip_ranges.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.24.0 2 | netaddr==0.8.0 3 | lxml==4.6.3 4 | coloredlogs==14.0 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 NCC Group Plc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloud IP Ranges 2 | 3 | ## Description 4 | 5 | Most cloud providers publish up to date lists of their IP address ranges. This tools identifies if an IP belongs to a provider's ranges by fetching and parsing the latest lists. 6 | 7 | Supports: 8 | 9 | - [x] AWS ([source](https://ip-ranges.amazonaws.com/ip-ranges.json)) 10 | - [x] Azure ([source](https://www.microsoft.com/en-us/download/confirmation.aspx?id=56519)) 11 | - [x] Google Cloud Platform ([source](https://www.gstatic.com/ipranges/cloud.json)) 12 | - [ ] Alibaba Cloud (currently doesn't publish lists) 13 | - [x] Oracle Cloud Infrastructure ([source](https://docs.cloud.oracle.com/en-us/iaas/tools/public_ip_ranges.json)) 14 | - [ ] IBM Cloud (currently doesn't publish lists) 15 | - [x] DigitalOcean ([source](http://digitalocean.com/geo/google.csv)) 16 | 17 | This tool is inspired by [Nimbusland](https://gist.github.com/TweekFawkes/ff83fe294f82f6d73c3ad14697e43ad5) by [Bryce Kunz](http://www.brycekunz.com/). 18 | 19 | ## Usage 20 | 21 | The preferred installation method is with [`pipx`](https://pipxproject.github.io/pipx/): 22 | 23 | ```shell script 24 | $ pipx install https://github.com/nccgroup/cloud_ip_ranges 25 | $ cloud_ip_ranges 26 | ``` 27 | 28 | Alternatively, you can setup a virtual environment and install dependencies: 29 | 30 | ```shell script 31 | $ virtualenv -p python3 venv 32 | $ source venv/bin/activate 33 | $ pip install -r requirements.txt 34 | ``` 35 | 36 | Run the tool: 37 | 38 | ```shell script 39 | $ cloud_ip_ranges -h 40 | 41 | usage: cloud_ip_ranges [-h] [-q] ip 42 | 43 | positional arguments: 44 | ip The IP to evaluate, e.g.: 8.8.8.8 45 | 46 | optional arguments: 47 | -h, --help show this help message and exit 48 | -q, --quiet Suppress logging output 49 | 50 | $ cloud_ip_ranges 52.4.0.0 51 | 52 | 2020-09-18 17:38:42 host __main__[21549] INFO Starting 53 | 2020-09-18 17:38:42 host __main__[21549] INFO Checking for AWS 54 | 2020-09-18 17:38:43 host __main__[21549] INFO Match for AWS range "52.4.0.0/14", region "us-east-1" and service "AMAZON" 55 | 2020-09-18 17:38:43 host __main__[21549] INFO Match for AWS range "52.4.0.0/14", region "us-east-1" and service "EC2" 56 | 2020-09-18 17:38:43 host __main__[21549] INFO Checking for Azure 57 | 2020-09-18 17:38:44 host __main__[21549] INFO Checking for GCP 58 | 2020-09-18 17:38:44 host __main__[21549] INFO Checking for OCI 59 | 2020-09-18 17:38:44 host __main__[21549] INFO Done 60 | ``` 61 | -------------------------------------------------------------------------------- /.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 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 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 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 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 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # intellij 141 | .idea 142 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """A setuptools based setup module. 2 | 3 | See: 4 | https://packaging.python.org/guides/distributing-packages-using-setuptools/ 5 | https://github.com/pypa/sampleproject 6 | """ 7 | 8 | # Always prefer setuptools over distutils 9 | from setuptools import setup, find_packages 10 | import pathlib 11 | 12 | here = pathlib.Path(__file__).parent.resolve() 13 | 14 | # Get the long description from the README file 15 | long_description = (here / "README.md").read_text(encoding="utf-8") 16 | 17 | # Arguments marked as "Required" below must be included for upload to PyPI. 18 | # Fields marked as "Optional" may be commented out. 19 | 20 | setup( 21 | name="cloud_ip_ranges", # Required 22 | version="1.0.0", 23 | # This is a one-line description or tagline of what your project does. This 24 | # corresponds to the "Summary" metadata field: 25 | # https://packaging.python.org/specifications/core-metadata/#summary 26 | description="This tools identifies if an IP belongs to a provider's ranges by fetching and parsing the latest lists.", 27 | long_description=long_description, 28 | long_description_content_type="text/markdown", 29 | url="https://github.com/nccgroup/cloud_ip_ranges", 30 | author="Xavier Garceau-Aranda", 31 | author_email="xavier.garceau-aranda@owasp.org", 32 | # Classifiers help users find your project by categorizing it. 33 | # 34 | # For a list of valid classifiers, see https://pypi.org/classifiers/ 35 | classifiers=[ # Optional 36 | # How mature is this project? Common values are 37 | # 3 - Alpha 38 | # 4 - Beta 39 | # 5 - Production/Stable 40 | "Development Status :: 3 - Beta", 41 | "Intended Audience :: Developers", 42 | # Pick your license as you wish 43 | "License :: OSI Approved :: MIT License", 44 | "Programming Language :: Python :: 3", 45 | "Programming Language :: Python :: 3.7", 46 | "Programming Language :: Python :: 3.8", 47 | "Programming Language :: Python :: 3.9", 48 | "Programming Language :: Python :: 3 :: Only", 49 | ], 50 | # You can just specify package directories manually here if your project is 51 | # simple. Or you can use find_packages(). 52 | # 53 | # Alternatively, if you just want to distribute a single Python file, use 54 | # the `py_modules` argument instead as follows, which will expect a file 55 | # called `my_module.py` to exist: 56 | # 57 | # py_modules=["my_module"], 58 | # 59 | py_modules=["cloud_ip_ranges"], 60 | # packages=find_packages(where='src'), # Required 61 | # Specify which Python versions you support. In contrast to the 62 | # 'Programming Language' classifiers above, 'pip install' will check this 63 | # and refuse to install the project if the version does not match. See 64 | # https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires 65 | python_requires=">=3.5, <4", 66 | # This field lists other packages that your project depends on to run. 67 | # Any package you put here will be installed by pip when your project is 68 | # installed, so they must be valid existing projects. 69 | # 70 | # For an analysis of "install_requires" vs pip's requirements files see: 71 | # https://packaging.python.org/en/latest/requirements.html 72 | install_requires=["requests", "netaddr", "lxml", "coloredlogs"], 73 | # To provide executable scripts, use entry points in preference to the 74 | # "scripts" keyword. Entry points provide cross-platform support and allow 75 | # `pip` to create the appropriate form of executable for the target 76 | # platform. 77 | # 78 | # For example, the following would provide a command called `sample` which 79 | # executes the function `main` from this package when invoked: 80 | entry_points={ # Optional 81 | "console_scripts": ["cloud_ip_ranges=cloud_ip_ranges:main",], 82 | }, 83 | ) 84 | -------------------------------------------------------------------------------- /cloud_ip_ranges.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from argparse import ArgumentParser 4 | import requests 5 | from netaddr import IPNetwork, IPAddress 6 | from lxml import html 7 | import csv 8 | import coloredlogs 9 | import logging 10 | 11 | 12 | def match_aws(target_ip): 13 | matched = False 14 | try: 15 | logger.info('Checking for AWS') 16 | aws_url = 'https://ip-ranges.amazonaws.com/ip-ranges.json' 17 | aws_ips = requests.get(aws_url, allow_redirects=True).json() 18 | 19 | for item in aws_ips["prefixes"]: 20 | if target_ip in IPNetwork(str(item["ip_prefix"])): 21 | matched = True 22 | logger.info(f'Match for AWS range ' 23 | f'"{item["ip_prefix"]}", region "{item["region"]}" and service "{item["service"]}"') 24 | 25 | except Exception as e: 26 | logger.error(f'Error: {e}') 27 | 28 | return matched 29 | 30 | 31 | def match_azure(target_ip): 32 | matched = False 33 | try: 34 | logger.info('Checking for Azure') 35 | azure_url = 'https://www.microsoft.com/en-us/download/confirmation.aspx?id=56519' 36 | page = requests.get(azure_url) 37 | tree = html.fromstring(page.content) 38 | download_url = tree.xpath("//a[contains(@class, 'failoverLink') and " 39 | "contains(@href,'download.microsoft.com/download/')]/@href")[0] 40 | 41 | azure_ips = requests.get(download_url, allow_redirects=True).json() 42 | 43 | for item in azure_ips["values"]: 44 | for prefix in item["properties"]['addressPrefixes']: 45 | if target_ip in IPNetwork(str(prefix)): 46 | matched = True 47 | logger.info(f'Match for Azure range "{prefix}", region "{item["properties"]["region"]}" and ' 48 | f'service "{item["properties"]["systemService"]}"') 49 | 50 | except Exception as e: 51 | logger.error(f'Error: {e}') 52 | 53 | return matched 54 | 55 | 56 | def match_gcp(target_ip): 57 | matched = False 58 | try: 59 | logger.info('Checking for GCP') 60 | gcp_url = 'https://www.gstatic.com/ipranges/cloud.json' 61 | gcp_ips = requests.get(gcp_url, allow_redirects=True).json() 62 | 63 | for item in gcp_ips["prefixes"]: 64 | if target_ip in IPNetwork(str(item.get("ipv4Prefix", item.get("ipv6Prefix")))): 65 | matched = True 66 | logger.info(f'Match for GCP range "{item.get("ipv4Prefix", item.get("ipv6Prefix"))}", ' 67 | f'region "{item["scope"]}" and service "{item["service"]}"') 68 | 69 | except Exception as e: 70 | logger.error(f'Error: {e}') 71 | 72 | return matched 73 | 74 | 75 | def match_oci(target_ip): 76 | matched = False 77 | try: 78 | logger.info('Checking for OCI') 79 | oci_url = 'https://docs.cloud.oracle.com/en-us/iaas/tools/public_ip_ranges.json' 80 | oci_ips = requests.get(oci_url, allow_redirects=True).json() 81 | 82 | for region in oci_ips["regions"]: 83 | for cidr_item in region['cidrs']: 84 | if target_ip in IPNetwork(str(cidr_item["cidr"])): 85 | matched = True 86 | logger.info(f'Match for OCI range "{cidr_item["cidr"]}", region "{region["region"]}" and ' 87 | f'service "{cidr_item["tags"][-1]}"') 88 | 89 | except Exception as e: 90 | logger.error(f'Error: {e}') 91 | 92 | return matched 93 | 94 | 95 | def match_do(target_ip): 96 | matched = False 97 | try: 98 | logger.info('Checking for DigitalOcean') 99 | 100 | # This is the file linked from the digitalocean platform documentation website: 101 | # https://www.digitalocean.com/docs/platform/ 102 | do_url = 'http://digitalocean.com/geo/google.csv' 103 | do_ips_request = requests.get(do_url, allow_redirects=True) 104 | 105 | do_ips = csv.DictReader(do_ips_request.content.decode('utf-8').splitlines(), fieldnames=[ 106 | 'range', 'country', 'region', 'city', 'postcode' 107 | ]) 108 | 109 | for item in do_ips: 110 | if target_ip in IPNetwork(item['range']): 111 | matched = True 112 | logger.info(f'Match for DigitalOcean range "{item["range"]}", country "{item["country"]}", ' 113 | f'state "{item["region"]}" and address "{item["city"]} {item["postcode"]}"') 114 | 115 | except Exception as e: 116 | logger.error(f'Error: {e}') 117 | 118 | return matched 119 | 120 | 121 | logger = logging.getLogger(__name__) 122 | coloredlogs.install(level='info') 123 | 124 | def main(): 125 | parser = ArgumentParser(add_help=True, allow_abbrev=False) 126 | 127 | parser.add_argument('-q', '--quiet', dest='quiet', action='store_true', 128 | help="Suppress logging output") 129 | parser.add_argument('ip', 130 | help="The IP to evaluate, e.g.: 8.8.8.8") 131 | 132 | args = parser.parse_args() 133 | 134 | if args.quiet: 135 | logger.setLevel('CRITICAL') 136 | 137 | target_ip = IPAddress(args.ip) 138 | 139 | logger.info(f'Starting IP check for: {target_ip}') 140 | 141 | matches = [ 142 | match_aws(target_ip), 143 | match_azure(target_ip), 144 | match_gcp(target_ip), 145 | match_oci(target_ip), 146 | match_do(target_ip) 147 | ] 148 | 149 | logger.info('Done') 150 | 151 | if any(matches): 152 | exit(1) 153 | else: 154 | exit(0) 155 | 156 | if __name__ == "__main__": 157 | main() --------------------------------------------------------------------------------