|
| 1 | +#!/usr/bin/python3 |
| 2 | +import sys |
| 3 | +from argparse import ArgumentParser |
| 4 | +from typing import Dict, Any, Optional, List |
| 5 | +from bitdiscovery.api import BitDiscoveryApi, try_multiple_times, get_lastid |
| 6 | +from bitdiscovery.cloud import get_provider, remove_matches, CloudProvider, AWSProvider |
| 7 | + |
| 8 | +parser = ArgumentParser(description="Add your cloud provider assets to your Bit Discovery inventory.") |
| 9 | +parser.add_argument('cloudprovider', metavar="PROVIDER", type=str, choices=['amazon-ec2', 'google-cloud', 'azure'], |
| 10 | + help="The cloud provider to add assets from, either amazon-ec2, google-cloud or azure.") |
| 11 | +parser.add_argument('apikey', metavar="APIKEY", type=str, help="Your Bit Discovery API key.") |
| 12 | +parser.add_argument('--env', choices=['dev', 'staging', 'prod'], default="dev", |
| 13 | + help="The Bit Discovery environment (by default 'dev')") |
| 14 | +parser.add_argument('--offset', type=int, default=0, help="Offset to the API request data (by default 0).") |
| 15 | +parser.add_argument('--limit', type=int, default=5000, help="Limit to the API request data (by default 500).") |
| 16 | +args = parser.parse_args() |
| 17 | + |
| 18 | +APIKEY: str = args.apikey |
| 19 | +CLOUD_PROVIDER: str = args.cloudprovider |
| 20 | +APIURL: str = "https://bitdiscovery.com/api/1.0" |
| 21 | +OFFSET: int = args.offset |
| 22 | +LIMIT: int = args.limit |
| 23 | + |
| 24 | + |
| 25 | +# Takes two dicts and safely merges them into a copy |
| 26 | +def merge_two_dicts(x: Dict, y: Dict) -> Dict: |
| 27 | + z = x.copy() # start with x's keys and values |
| 28 | + z.update(y) # modifies z with y's keys and values & returns None |
| 29 | + return z |
| 30 | + |
| 31 | + |
| 32 | +# Find all IPs belonging in Bit Discovery |
| 33 | +print("Initializing and pulling assets from Bit Discovery...") |
| 34 | + |
| 35 | +api = BitDiscoveryApi(APIURL, APIKEY) |
| 36 | +inventories_json: Dict[str, Any] = {} |
| 37 | +try: |
| 38 | + inventories_json = api.find_inventories(OFFSET, LIMIT) |
| 39 | +except: |
| 40 | + print("API call failed. Try again later.") |
| 41 | + exit(1) |
| 42 | + |
| 43 | +# TODO: maybe remove iteration if we cannot add to multiple inventories |
| 44 | +inventories: Dict[str, str] = {inventories_json['actualInventory']['inventory_name']: APIKEY} |
| 45 | + |
| 46 | +for entityname in inventories: |
| 47 | + jsondata = [] |
| 48 | + # TODO: this is never used, why do we have to keep this (merge_two_dicts too) |
| 49 | + inventoryips = {} |
| 50 | + |
| 51 | + print(f"Starting sources for: {entityname}.") |
| 52 | + |
| 53 | + sourcesdata: List[Dict[str, Any]] = [] |
| 54 | + |
| 55 | + # Collect every source from Bit Discovery inventory with pagination |
| 56 | + lastid: str = '' |
| 57 | + offset: int = OFFSET |
| 58 | + while True: |
| 59 | + result: Optional[Dict[str, Any]] = try_multiple_times( |
| 60 | + lambda: api.search_for_source(LIMIT, lastid, ""), |
| 61 | + max_tries=5 |
| 62 | + ) |
| 63 | + |
| 64 | + if result is None: |
| 65 | + print("\tAPI call failed too many times. Try again later.") |
| 66 | + exit(1) |
| 67 | + |
| 68 | + sourcesdata.append(result) |
| 69 | + lastid = get_lastid(result) |
| 70 | + offset += LIMIT |
| 71 | + total: int = int(sourcesdata[0]['total']) |
| 72 | + |
| 73 | + if offset > total: |
| 74 | + break |
| 75 | + |
| 76 | + # Collect all source IPs that aren't CIDRs |
| 77 | + sourceips: Dict[str, int] = {} |
| 78 | + for sources in sourcesdata: |
| 79 | + for source in sources.get('searches', []): |
| 80 | + if 'search_type' in source and source['search_type'] == 'iprange': |
| 81 | + ipcandidate = source['keyword'].lower() |
| 82 | + if '-' in ipcandidate or '/' in ipcandidate: |
| 83 | + continue |
| 84 | + else: |
| 85 | + # Number 2 indicates it's a source, not an asset |
| 86 | + sourceips[ipcandidate] = 2 |
| 87 | + |
| 88 | + # TODO: if inventoryips is to be deleted this can be removed as well |
| 89 | + superset: Dict[str, int] = merge_two_dicts(sourceips, inventoryips) |
| 90 | + for ips in superset: |
| 91 | + if ips in sourceips and ips in inventoryips: |
| 92 | + # Number 3 saying the IP address is found in both |
| 93 | + superset[ips] = 3 |
| 94 | + |
| 95 | + # Find all IPs in cloud |
| 96 | + addednum = 0 |
| 97 | + |
| 98 | + provider: CloudProvider = get_provider(CLOUD_PROVIDER) |
| 99 | + |
| 100 | + # Get provider IP ranges from the provider |
| 101 | + # TODO: why is this read? we don't use this for anything |
| 102 | + print(f"\tWe're on {provider.name}, so processing accordingly") |
| 103 | + print(f"\t\tGetting and parsing all of {provider.name}'s public IP space") |
| 104 | + prefixes: Dict[str, int] = provider.get_ip_ranges() |
| 105 | + |
| 106 | + # Get your ips from the provider |
| 107 | + print("\t\tGetting and parsing your public IPs") |
| 108 | + ips: Dict[str, int] = provider.get_instance_ips() |
| 109 | + |
| 110 | + # If IPs in cloud match Bit Discovery remove them from list to do further checks on (they haven't changed) |
| 111 | + print("\t\tIgnorning assets that haven't changed.") |
| 112 | + ips_new, old_ips = remove_matches(superset, inventoryips, sourceips, ips) |
| 113 | + |
| 114 | + # If IPs are not in Bit Discovery but they are in cloud add them |
| 115 | + print("\t\tAdding new IPs") |
| 116 | + for new_ip in ips_new: |
| 117 | + # Try to add the new IPs to the Bit Discovery inventory |
| 118 | + result: Optional[bool] = try_multiple_times( |
| 119 | + lambda: api.add_ip(new_ip), |
| 120 | + max_tries=5 |
| 121 | + ) |
| 122 | + |
| 123 | + if result is None: |
| 124 | + print("\tAPI call failed too many times. Try again later.") |
| 125 | + exit(1) |
| 126 | + |
| 127 | + # Increment added count |
| 128 | + addednum += 1 |
| 129 | + |
| 130 | + print(f"\tAdded a total of {str(addednum)} {provider.name} IPs.") |
| 131 | + |
| 132 | + # If provider is AWS, then we can also retrieve the buckets |
| 133 | + if type(provider) == AWSProvider: |
| 134 | + print("\t\tFinding s3 buckets.") |
| 135 | + buckets = AWSProvider().find_s3_buckets() |
| 136 | + |
| 137 | + print("\t\tAdding s3 buckets.") |
| 138 | + addedbucket = 0 |
| 139 | + for bucket in buckets: |
| 140 | + url = AWSProvider().find_s3_region(bucket) |
| 141 | + |
| 142 | + # Try to add bucket URLs to the Bit Discovery inventory |
| 143 | + success: Optional[bool] = try_multiple_times( |
| 144 | + lambda: api.add_source(url), |
| 145 | + max_tries=5 |
| 146 | + ) |
| 147 | + |
| 148 | + if result is None: |
| 149 | + print("\tAPI call failed too many times. Try again later.") |
| 150 | + exit(1) |
| 151 | + |
| 152 | + # Increment bucket count |
| 153 | + addedbucket += 1 |
| 154 | + |
| 155 | + print("\tAdded a total of " + str(addedbucket) + " S3 buckets.") |
| 156 | + |
| 157 | +print("Done.") |
0 commit comments