Skip to content

Commit

Permalink
Add new CLI command: shodan alert download [--alert-id=] <filename>
Browse files Browse the repository at this point in the history
  • Loading branch information
achillean committed Jan 25, 2021
1 parent 9049130 commit 5eedf29
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 1 deletion.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
CHANGELOG
=========

1.25.0
------
* Add new CLI command: shodan alert download

1.24.0
------
* Add new CLI command: shodan alert stats

1.23.0
------
* Add new CLI command: shodan alert domain
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

setup(
name='shodan',
version='1.24.0',
version='1.25.0',
description='Python library and command-line utility for Shodan (https://developer.shodan.io)',
long_description=README,
long_description_content_type='text/x-rst',
Expand Down
83 changes: 83 additions & 0 deletions shodan/cli/alert.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

from collections import defaultdict
from operator import itemgetter
from shodan import APIError
from shodan.cli.helpers import get_api_key
from shodan.helpers import open_file, write_banner
from time import sleep


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


@alert.command(name='download')
@click.argument('filename', metavar='<filename>', type=str)
@click.option('--alert-id', help='Specific alert ID to download the data of', default=None)
def alert_download(filename, alert_id):
"""Download all information for monitored networks/ IPs."""
key = get_api_key()

api = shodan.Shodan(key)
ips = set()
networks = set()

# Helper method to process batches of IPs
def batch(iterable, size=1):
iter_length = len(iterable)
for ndx in range(0, iter_length, size):
yield iterable[ndx:min(ndx + size, iter_length)]

try:
# Get the list of alerts for the user
click.echo('Looking up alert information...')
if alert_id:
alerts = [api.alerts(aid=alert_id.strip())]
else:
alerts = api.alerts()

click.echo('Compiling list of networks/ IPs to download...')
for alert in alerts:
for net in alert['filters']['ip']:
if '/' in net:
networks.add(net)
else:
ips.add(net)

click.echo('Downloading...')
with open_file(filename) as fout:
# Check if the user is able to use batch IP lookups
batch_size = 1
if len(ips) > 0:
api_info = api.info()
if api_info['plan'] in ['corp', 'stream-100']:
batch_size = 100

# Convert it to a list so we can index into it
ips = list(ips)

# Grab all the IP information
for ip in batch(ips, size=batch_size):
try:
click.echo(ip)
results = api.host(ip)
if not isinstance(results, list):
results = [results]

for host in results:
for banner in host['data']:
write_banner(fout, banner)
except APIError:
pass
sleep(1) # Slow down a bit to make sure we don't hit the rate limit

# Grab all the network ranges
for net in networks:
try:
counter = 0
click.echo(net)
for banner in api.search_cursor('net:{}'.format(net)):
write_banner(fout, banner)

# Slow down a bit to make sure we don't hit the rate limit
if counter % 100 == 0:
sleep(1)
counter += 1
except APIError:
pass
except shodan.APIError as e:
raise click.ClickException(e.value)

click.secho('Successfully downloaded results into: {}'.format(filename), fg='green')


@alert.command(name='info')
@click.argument('alert', metavar='<alert id>')
def alert_info(alert):
Expand Down

0 comments on commit 5eedf29

Please sign in to comment.