|
4 | 4 |
|
5 | 5 | from collections import defaultdict |
6 | 6 | from operator import itemgetter |
| 7 | +from shodan import APIError |
7 | 8 | from shodan.cli.helpers import get_api_key |
| 9 | +from shodan.helpers import open_file, write_banner |
| 10 | +from time import sleep |
8 | 11 |
|
9 | 12 |
|
10 | 13 | MAX_QUERY_LENGTH = 1000 |
@@ -138,6 +141,86 @@ def alert_domain(domain, triggers): |
138 | 141 | click.secho('Alert ID: {}'.format(alert['id']), fg='cyan') |
139 | 142 |
|
140 | 143 |
|
| 144 | +@alert.command(name='download') |
| 145 | +@click.argument('filename', metavar='<filename>', type=str) |
| 146 | +@click.option('--alert-id', help='Specific alert ID to download the data of', default=None) |
| 147 | +def alert_download(filename, alert_id): |
| 148 | + """Download all information for monitored networks/ IPs.""" |
| 149 | + key = get_api_key() |
| 150 | + |
| 151 | + api = shodan.Shodan(key) |
| 152 | + ips = set() |
| 153 | + networks = set() |
| 154 | + |
| 155 | + # Helper method to process batches of IPs |
| 156 | + def batch(iterable, size=1): |
| 157 | + iter_length = len(iterable) |
| 158 | + for ndx in range(0, iter_length, size): |
| 159 | + yield iterable[ndx:min(ndx + size, iter_length)] |
| 160 | + |
| 161 | + try: |
| 162 | + # Get the list of alerts for the user |
| 163 | + click.echo('Looking up alert information...') |
| 164 | + if alert_id: |
| 165 | + alerts = [api.alerts(aid=alert_id.strip())] |
| 166 | + else: |
| 167 | + alerts = api.alerts() |
| 168 | + |
| 169 | + click.echo('Compiling list of networks/ IPs to download...') |
| 170 | + for alert in alerts: |
| 171 | + for net in alert['filters']['ip']: |
| 172 | + if '/' in net: |
| 173 | + networks.add(net) |
| 174 | + else: |
| 175 | + ips.add(net) |
| 176 | + |
| 177 | + click.echo('Downloading...') |
| 178 | + with open_file(filename) as fout: |
| 179 | + # Check if the user is able to use batch IP lookups |
| 180 | + batch_size = 1 |
| 181 | + if len(ips) > 0: |
| 182 | + api_info = api.info() |
| 183 | + if api_info['plan'] in ['corp', 'stream-100']: |
| 184 | + batch_size = 100 |
| 185 | + |
| 186 | + # Convert it to a list so we can index into it |
| 187 | + ips = list(ips) |
| 188 | + |
| 189 | + # Grab all the IP information |
| 190 | + for ip in batch(ips, size=batch_size): |
| 191 | + try: |
| 192 | + click.echo(ip) |
| 193 | + results = api.host(ip) |
| 194 | + if not isinstance(results, list): |
| 195 | + results = [results] |
| 196 | + |
| 197 | + for host in results: |
| 198 | + for banner in host['data']: |
| 199 | + write_banner(fout, banner) |
| 200 | + except APIError: |
| 201 | + pass |
| 202 | + sleep(1) # Slow down a bit to make sure we don't hit the rate limit |
| 203 | + |
| 204 | + # Grab all the network ranges |
| 205 | + for net in networks: |
| 206 | + try: |
| 207 | + counter = 0 |
| 208 | + click.echo(net) |
| 209 | + for banner in api.search_cursor('net:{}'.format(net)): |
| 210 | + write_banner(fout, banner) |
| 211 | + |
| 212 | + # Slow down a bit to make sure we don't hit the rate limit |
| 213 | + if counter % 100 == 0: |
| 214 | + sleep(1) |
| 215 | + counter += 1 |
| 216 | + except APIError: |
| 217 | + pass |
| 218 | + except shodan.APIError as e: |
| 219 | + raise click.ClickException(e.value) |
| 220 | + |
| 221 | + click.secho('Successfully downloaded results into: {}'.format(filename), fg='green') |
| 222 | + |
| 223 | + |
141 | 224 | @alert.command(name='info') |
142 | 225 | @click.argument('alert', metavar='<alert id>') |
143 | 226 | def alert_info(alert): |
|
0 commit comments