Skip to content

Commit 5eedf29

Browse files
committed
Add new CLI command: shodan alert download [--alert-id=] <filename>
1 parent 9049130 commit 5eedf29

File tree

3 files changed

+92
-1
lines changed

3 files changed

+92
-1
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
CHANGELOG
22
=========
33

4+
1.25.0
5+
------
6+
* Add new CLI command: shodan alert download
7+
8+
1.24.0
9+
------
10+
* Add new CLI command: shodan alert stats
11+
412
1.23.0
513
------
614
* Add new CLI command: shodan alert domain

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
setup(
1111
name='shodan',
12-
version='1.24.0',
12+
version='1.25.0',
1313
description='Python library and command-line utility for Shodan (https://developer.shodan.io)',
1414
long_description=README,
1515
long_description_content_type='text/x-rst',

shodan/cli/alert.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
from collections import defaultdict
66
from operator import itemgetter
7+
from shodan import APIError
78
from shodan.cli.helpers import get_api_key
9+
from shodan.helpers import open_file, write_banner
10+
from time import sleep
811

912

1013
MAX_QUERY_LENGTH = 1000
@@ -138,6 +141,86 @@ def alert_domain(domain, triggers):
138141
click.secho('Alert ID: {}'.format(alert['id']), fg='cyan')
139142

140143

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+
141224
@alert.command(name='info')
142225
@click.argument('alert', metavar='<alert id>')
143226
def alert_info(alert):

0 commit comments

Comments
 (0)