diff --git a/shodan/__main__.py b/shodan/__main__.py index 4093b94..4c1e357 100644 --- a/shodan/__main__.py +++ b/shodan/__main__.py @@ -45,7 +45,7 @@ from shodan.cli.settings import SHODAN_CONFIG_DIR, COLORIZE_FIELDS # Helper methods -from shodan.cli.helpers import async_spinner, get_api_key, escape_data, timestr, open_streaming_file, get_banner_field, match_filters +from shodan.cli.helpers import async_spinner, get_shodan_inst, escape_data, timestr, open_streaming_file, get_banner_field, match_filters from shodan.cli.host import HOST_PRINT # Allow 3rd-parties to develop custom commands @@ -84,7 +84,7 @@ def main(): pass -# Setup the large subcommands +# Set up the large subcommands main.add_command(alert) main.add_command(data) main.add_command(org) @@ -147,11 +147,9 @@ def convert(fields, input, format): @click.option('--save', '-S', help='Save the information in the a file named after the domain (append if file exists).', default=False, is_flag=True) @click.option('--history', '-H', help='Include historical DNS data in the results', default=False, is_flag=True) @click.option('--type', '-T', help='Only returns DNS records of the provided type', default=None) -def domain_info(domain, details, save, history, type): +@get_shodan_inst +def domain_info(domain, details, save, history, type, api): """View all available information for a domain""" - key = get_api_key() - api = shodan.Shodan(key) - try: info = api.dns.domain_info(domain, history=history, type=type) except shodan.APIError as e: @@ -239,10 +237,9 @@ def init(key): @main.command() @click.argument('query', metavar='', nargs=-1) -def count(query): +@get_shodan_inst +def count(query, api): """Returns the number of results for a search""" - key = get_api_key() - # Create the query string out of the provided tuple query = ' '.join(query).strip() @@ -251,7 +248,6 @@ def count(query): raise click.ClickException('Empty search query') # Perform the search - api = shodan.Shodan(key) try: results = api.count(query) except shodan.APIError as e: @@ -265,10 +261,9 @@ def count(query): @click.option('--limit', help='The number of results you want to download. -1 to download all the data possible.', default=1000, type=int) @click.argument('filename', metavar='') @click.argument('query', metavar='', nargs=-1) -def download(fields, limit, filename, query): +@get_shodan_inst +def download(fields, limit, filename, query, api): """Download search results and save them in a compressed JSON file.""" - key = get_api_key() - # Create the query string out of the provided tuple query = ' '.join(query).strip() @@ -289,8 +284,6 @@ def download(fields, limit, filename, query): fields = [item.strip() for item in fields.split(',')] # Perform the search - api = shodan.Shodan(key) - try: total = api.count(query)['total'] info = api.info() @@ -336,11 +329,9 @@ def download(fields, limit, filename, query): @click.option('--filename', '-O', help='Save the host information in the given file (append if file exists).', default=None) @click.option('--save', '-S', help='Save the host information in the a file named after the IP (append if file exists).', default=False, is_flag=True) @click.argument('ip', metavar='') -def host(format, history, filename, save, ip): +@get_shodan_inst +def host(format, history, filename, save, ip, api): """View all available information for an IP address""" - key = get_api_key() - api = shodan.Shodan(key) - try: host = api.host(ip, history=history) @@ -367,10 +358,9 @@ def host(format, history, filename, save, ip): @main.command() -def info(): +@get_shodan_inst +def info(api): """Shows general information about your account""" - key = get_api_key() - api = shodan.Shodan(key) try: results = api.info() except shodan.APIError as e: @@ -398,7 +388,7 @@ def parse(color, fields, filters, filename, separator, filenames): has_filters = len(filters) > 0 - # Setup the output file handle + # Set up the output file handle fout = None if filename: # If no filters were provided raise an error since it doesn't make much sense w/out them @@ -450,12 +440,9 @@ def parse(color, fields, filters, filename, separator, filenames): @main.command() @click.option('--ipv6', '-6', is_flag=True, default=False, help='Try to use IPv6 instead of IPv4') -def myip(ipv6): +@get_shodan_inst +def myip(ipv6, api): """Print your external IP address""" - key = get_api_key() - - api = shodan.Shodan(key) - # Use the IPv6-enabled domain if requested if ipv6: api.base_url = 'https://apiv6.shodan.io' @@ -472,10 +459,9 @@ def myip(ipv6): @click.option('--limit', help='The number of search results that should be returned. Maximum: 1000', default=100, type=int) @click.option('--separator', help='The separator between the properties of the search results.', default='\t') @click.argument('query', metavar='', nargs=-1) -def search(color, fields, limit, separator, query): +@get_shodan_inst +def search(color, fields, limit, separator, query, api): """Search the Shodan database""" - key = get_api_key() - # Create the query string out of the provided tuple query = ' '.join(query).strip() @@ -494,7 +480,6 @@ def search(color, fields, limit, separator, query): raise click.ClickException('Please define at least one property to show') # Perform the search - api = shodan.Shodan(key) try: results = api.search(query, limit=limit, minify=False, fields=fields) except shodan.APIError as e: @@ -543,12 +528,9 @@ def search(color, fields, limit, separator, query): @click.option('--facets', help='List of facets to get statistics for.', default='country,org') @click.option('--filename', '-O', help='Save the results in a CSV file of the provided name.', default=None) @click.argument('query', metavar='', nargs=-1) -def stats(limit, facets, filename, query): +@get_shodan_inst +def stats(limit, facets, filename, query, api): """Provide summary information about a search query""" - # Setup Shodan - key = get_api_key() - api = shodan.Shodan(key) - # Create the query string out of the provided tuple query = ' '.join(query).strip() @@ -644,12 +626,11 @@ def stats(limit, facets, filename, query): @click.option('--timeout', help='Timeout. Should the shodan stream cease to send data, then timeout after seconds.', default=0, type=int) @click.option('--color/--no-color', default=True) @click.option('--quiet', help='Disable the printing of information to the screen.', is_flag=True) -def stream(streamer, fields, separator, datadir, asn, alert, countries, custom_filters, ports, tags, vulns, limit, compresslevel, timeout, color, quiet): +@get_shodan_inst +def stream(streamer, fields, separator, datadir, asn, alert, countries, custom_filters, ports, tags, vulns, limit, + compresslevel, timeout, color, quiet, api + ): """Stream data in real-time.""" - # Setup the Shodan API - key = get_api_key() - api = shodan.Shodan(key) - # Temporarily change the baseurl api.stream.base_url = streamer @@ -720,7 +701,7 @@ def stream(streamer, fields, separator, datadir, asn, alert, countries, custom_f else: stream_type = 'all' - # Decide which stream to subscribe to based on whether or not ports were selected + # Decide which stream to subscribe to based on whether ports were selected def _create_stream(name, args, timeout): return { 'all': api.stream.banners(timeout=timeout), @@ -810,11 +791,9 @@ def _create_stream(name, args, timeout): @click.option('--filename', '-O', help='Save the full results in the given file (append if file exists).', default=None) @click.option('--save', '-S', help='Save the full results in the a file named after the query (append if file exists).', default=False, is_flag=True) @click.argument('query', metavar='', nargs=-1) -def trends(filename, save, facets, query): +@get_shodan_inst +def trends(filename, save, facets, query, api): """Search Shodan historical database""" - key = get_api_key() - api = shodan.Shodan(key) - # Create the query string out of the provided tuple query = ' '.join(query).strip() facets = facets.strip() @@ -903,11 +882,9 @@ def trends(filename, save, facets, query): @main.command() @click.argument('ip', metavar='') -def honeyscore(ip): +@get_shodan_inst +def honeyscore(ip, api): """Check whether the IP is a honeypot or not.""" - key = get_api_key() - api = shodan.Shodan(key) - try: score = api.labs.honeyscore(ip) @@ -924,11 +901,9 @@ def honeyscore(ip): @main.command() -def radar(): +@get_shodan_inst +def radar(api): """Real-Time Map of some results as Shodan finds them.""" - key = get_api_key() - api = shodan.Shodan(key) - from shodan.cli.worldmap import launch_map try: diff --git a/shodan/cli/alert.py b/shodan/cli/alert.py index 1df11ea..600560c 100644 --- a/shodan/cli/alert.py +++ b/shodan/cli/alert.py @@ -9,7 +9,7 @@ from collections import defaultdict from operator import itemgetter from shodan import APIError -from shodan.cli.helpers import get_api_key +from shodan.cli.helpers import get_shodan_inst from shodan.helpers import open_file, write_banner from time import sleep @@ -82,12 +82,9 @@ def alert(): @alert.command(name='clear') -def alert_clear(): +@get_shodan_inst +def alert_clear(api): """Remove all alerts""" - key = get_api_key() - - # Get the list - api = shodan.Shodan(key) try: alerts = api.alerts() for alert in alerts: @@ -101,12 +98,9 @@ def alert_clear(): @alert.command(name='create') @click.argument('name', metavar='') @click.argument('netblocks', metavar='', nargs=-1) -def alert_create(name, netblocks): +@get_shodan_inst +def alert_create(name, netblocks, api): """Create a network alert to monitor an external network""" - key = get_api_key() - - # Get the list - api = shodan.Shodan(key) try: alert = api.create_alert(name, netblocks) except shodan.APIError as e: @@ -118,12 +112,13 @@ def alert_create(name, netblocks): @alert.command(name='domain') @click.argument('domain', metavar='', type=str) -@click.option('--triggers', help='List of triggers to enable', default='malware,industrial_control_system,internet_scanner,iot,open_database,new_service,ssl_expired,vulnerable') -def alert_domain(domain, triggers): +@click.option('--triggers', help='List of triggers to enable', + default='malware,industrial_control_system,internet_scanner,iot,open_database,new_service,' + 'ssl_expired,vulnerable' + ) +@get_shodan_inst +def alert_domain(domain, triggers, api): """Create a network alert based on a domain name""" - key = get_api_key() - - api = shodan.Shodan(key) try: # Grab a list of IPs for the domain domain = domain.lower() @@ -160,11 +155,9 @@ def alert_domain(domain, triggers): @alert.command(name='download') @click.argument('filename', metavar='', type=str) @click.option('--alert-id', help='Specific alert ID to download the data of', default=None) -def alert_download(filename, alert_id): +@get_shodan_inst +def alert_download(filename, alert_id, api): """Download all information for monitored networks/ IPs.""" - key = get_api_key() - - api = shodan.Shodan(key) ips = set() networks = set() @@ -181,7 +174,7 @@ def batch(iterable, size=1): 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']: @@ -189,7 +182,7 @@ def batch(iterable, size=1): 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 @@ -198,7 +191,7 @@ def batch(iterable, size=1): 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) @@ -209,14 +202,14 @@ def batch(iterable, size=1): 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: @@ -224,7 +217,7 @@ def batch(iterable, size=1): 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) @@ -233,18 +226,15 @@ def batch(iterable, size=1): pass except shodan.APIError as e: raise click.ClickException(e.value) - + click.secho('Successfully downloaded results into: {}'.format(filename), fg='green') @alert.command(name='export') @click.option('--filename', help='Name of the output file', default='shodan-alerts.json.gz', type=str) -def alert_export(filename): +@get_shodan_inst +def alert_export(filename, api): """Export the configuration of monitored networks/ IPs to be used by ``shodan alert import``.""" - # Setup the API wrapper - key = get_api_key() - api = shodan.Shodan(key) - try: # Get the list of alerts for the user click.echo('Looking up alert information...') @@ -256,18 +246,15 @@ def alert_export(filename): json.dump(alerts, fout) except Exception as e: raise click.ClickException(e.value) - + click.secho('Successfully exported monitored networks', fg='green') @alert.command(name='import') @click.argument('filename', metavar='') -def alert_import(filename): +@get_shodan_inst +def alert_import(filename, api): """Export the configuration of monitored networks/ IPs to be used by ``shodan alert import``.""" - # Setup the API wrapper - key = get_api_key() - api = shodan.Shodan(key) - # A mapping of the old notifier IDs to the new ones notifier_map = {} @@ -293,7 +280,7 @@ def alert_import(filename): if info.get('ignore', []): for whitelist in info['ignore']: api.ignore_alert_trigger_notification(alert['id'], trigger, whitelist['ip'], whitelist['port']) - + # Enable the notifiers for prev_notifier in item.get('notifiers', []): # We don't need to do anything for the default notifier as that @@ -303,7 +290,7 @@ def alert_import(filename): # Get the new notifier based on the ID of the old one notifier = notifier_map.get(prev_notifier['id']) - + # Create the notifier if it doesn't yet exist if notifier is None: notifier = api.notifier.create(prev_notifier['provider'], prev_notifier['args'], description=prev_notifier['description']) @@ -314,17 +301,15 @@ def alert_import(filename): api.add_alert_notifier(alert['id'], notifier['id']) except Exception as e: raise click.ClickException(e.value) - + click.secho('Successfully imported monitored networks', fg='green') @alert.command(name='info') @click.argument('alert', metavar='') -def alert_info(alert): +@get_shodan_inst +def alert_info(alert, api): """Show information about a specific alert""" - key = get_api_key() - api = shodan.Shodan(key) - try: info = api.alerts(aid=alert) except shodan.APIError as e: @@ -356,12 +341,9 @@ def alert_info(alert): @alert.command(name='list') @click.option('--expired', help='Whether or not to show expired alerts.', default=True, type=bool) -def alert_list(expired): +@get_shodan_inst +def alert_list(expired, api): """List all the active alerts""" - key = get_api_key() - - # Get the list - api = shodan.Shodan(key) try: results = api.alerts(include_expired=expired) except shodan.APIError as e: @@ -396,12 +378,9 @@ def alert_list(expired): @click.option('--limit', help='The number of results to return.', default=10, type=int) @click.option('--filename', '-O', help='Save the results in a CSV file of the provided name.', default=None) @click.argument('facets', metavar='', nargs=-1) -def alert_stats(limit, filename, facets): +@get_shodan_inst +def alert_stats(limit, filename, facets, api): """Show summary information about your monitored networks""" - # Setup Shodan - key = get_api_key() - api = shodan.Shodan(key) - # Make sure the user didn't supply an empty string if not facets: raise click.ClickException('No facets provided') @@ -483,12 +462,9 @@ def alert_stats(limit, filename, facets): @alert.command(name='remove') @click.argument('alert_id', metavar='') -def alert_remove(alert_id): +@get_shodan_inst +def alert_remove(alert_id, api): """Remove the specified alert""" - key = get_api_key() - - # Get the list - api = shodan.Shodan(key) try: api.delete_alert(alert_id) except shodan.APIError as e: @@ -497,12 +473,9 @@ def alert_remove(alert_id): @alert.command(name='triggers') -def alert_list_triggers(): +@get_shodan_inst +def alert_list_triggers(api): """List the available notification triggers""" - key = get_api_key() - - # Get the list - api = shodan.Shodan(key) try: results = api.alert_triggers() except shodan.APIError as e: @@ -530,12 +503,9 @@ def alert_list_triggers(): @alert.command(name='enable') @click.argument('alert_id', metavar='') @click.argument('trigger', metavar='') -def alert_enable_trigger(alert_id, trigger): +@get_shodan_inst +def alert_enable_trigger(alert_id, trigger, api): """Enable a trigger for the alert""" - key = get_api_key() - - # Get the list - api = shodan.Shodan(key) try: api.enable_alert_trigger(alert_id, trigger) except shodan.APIError as e: @@ -547,12 +517,9 @@ def alert_enable_trigger(alert_id, trigger): @alert.command(name='disable') @click.argument('alert_id', metavar='') @click.argument('trigger', metavar='') -def alert_disable_trigger(alert_id, trigger): +@get_shodan_inst +def alert_disable_trigger(alert_id, trigger, api): """Disable a trigger for the alert""" - key = get_api_key() - - # Get the list - api = shodan.Shodan(key) try: api.disable_alert_trigger(alert_id, trigger) except shodan.APIError as e: diff --git a/shodan/cli/data.py b/shodan/cli/data.py index 98d7852..6f2ca3c 100644 --- a/shodan/cli/data.py +++ b/shodan/cli/data.py @@ -3,7 +3,7 @@ import shodan import shodan.helpers as helpers -from shodan.cli.helpers import get_api_key +from shodan.cli.helpers import get_shodan_inst @click.group() @@ -14,12 +14,9 @@ def data(): @data.command(name='list') @click.option('--dataset', help='See the available files in the given dataset', default=None, type=str) -def data_list(dataset): +@get_shodan_inst +def data_list(dataset, api): """List available datasets or the files within those datasets.""" - # Setup the API connection - key = get_api_key() - api = shodan.Shodan(key) - if dataset: # Show the files within this dataset files = api.data.list_files(dataset) @@ -43,15 +40,16 @@ def data_list(dataset): @data.command(name='download') -@click.option('--chunksize', help='The size of the chunks that are downloaded into memory before writing them to disk.', default=1024, type=int) +@click.option('--chunksize', help='The size of the chunks that are downloaded into memory before ' + 'writing them to disk.', + default=1024, type=int + ) @click.option('--filename', '-O', help='Save the file as the provided filename instead of the default.') @click.argument('dataset', metavar='') @click.argument('name', metavar='') -def data_download(chunksize, filename, dataset, name): - # Setup the API connection - key = get_api_key() - api = shodan.Shodan(key) - +@get_shodan_inst +def data_download(chunksize, filename, dataset, name, api): + """Download a dataset, or a file within a dataset.""" # Get the file object that the user requested which will contain the URL and total file size file = None try: diff --git a/shodan/cli/helpers.py b/shodan/cli/helpers.py index bde2f07..5710281 100644 --- a/shodan/cli/helpers.py +++ b/shodan/cli/helpers.py @@ -7,7 +7,9 @@ import itertools import os import sys +from shodan import Shodan from ipaddress import ip_network, ip_address +from functools import wraps from .settings import SHODAN_CONFIG_DIR @@ -17,22 +19,28 @@ basestring = (str, ) # Python 3 -def get_api_key(): - '''Returns the API key of the current logged-in user.''' - shodan_dir = os.path.expanduser(SHODAN_CONFIG_DIR) - keyfile = shodan_dir + '/api_key' +def get_shodan_inst(func): - # If the file doesn't yet exist let the user know that they need to - # initialize the shodan cli - if not os.path.exists(keyfile): - raise click.ClickException('Please run "shodan init " before using this command') + @wraps(func) + def decorator(*args, **kwargs): + shodan_dir = os.path.expanduser(SHODAN_CONFIG_DIR) + keyfile = shodan_dir + '/api_key' - # Make sure it is a read-only file - if not oct(os.stat(keyfile).st_mode).endswith("600"): - os.chmod(keyfile, 0o600) + # If the file doesn't yet exist let the user know that they need to + # initialize the shodan cli + if not os.path.exists(keyfile): + raise click.ClickException('Please run "shodan init " before using this command') - with open(keyfile, 'r') as fin: - return fin.read().strip() + # Make sure it is a read-only file + if not oct(os.stat(keyfile).st_mode).endswith("600"): + os.chmod(keyfile, 0o600) + + with open(keyfile, 'r') as fin: + key = fin.read().strip() + + shodan_inst = Shodan(key=key) + return func(api=shodan_inst, **kwargs) + return decorator def escape_data(args): diff --git a/shodan/cli/organization.py b/shodan/cli/organization.py index 5fbb764..f8d5a21 100644 --- a/shodan/cli/organization.py +++ b/shodan/cli/organization.py @@ -1,7 +1,7 @@ import click import shodan -from shodan.cli.helpers import get_api_key, humanize_api_plan +from shodan.cli.helpers import get_shodan_inst, humanize_api_plan @click.group() @@ -10,14 +10,12 @@ def org(): pass -@org.command() +@org.command(name='add') @click.option('--silent', help="Don't send a notification to the user", default=False, is_flag=True) @click.argument('user', metavar='') -def add(silent, user): +@get_shodan_inst +def add(silent, user, api): """Add a new member""" - key = get_api_key() - api = shodan.Shodan(key) - try: api.org.add_member(user, notify=not silent) except shodan.APIError as e: @@ -26,11 +24,10 @@ def add(silent, user): click.secho('Successfully added the new member', fg='green') -@org.command() -def info(): +@org.command(name='info') +@get_shodan_inst +def info(api): """Show an overview of the organization""" - key = get_api_key() - api = shodan.Shodan(key) try: organization = api.org.info() except shodan.APIError as e: @@ -65,13 +62,11 @@ def info(): click.secho('No members yet', dim=True) -@org.command() +@org.command(name='remove') @click.argument('user', metavar='') -def remove(user): +@get_shodan_inst +def remove(user, api): """Remove and downgrade a member""" - key = get_api_key() - api = shodan.Shodan(key) - try: api.org.remove_member(user) except shodan.APIError as e: diff --git a/shodan/cli/scan.py b/shodan/cli/scan.py index cfc7aab..72b59d9 100644 --- a/shodan/cli/scan.py +++ b/shodan/cli/scan.py @@ -7,7 +7,7 @@ import threading import time -from shodan.cli.helpers import get_api_key, async_spinner +from shodan.cli.helpers import get_shodan_inst, async_spinner from shodan.cli.settings import COLORIZE_FIELDS @@ -18,12 +18,9 @@ def scan(): @scan.command(name='list') -def scan_list(): +@get_shodan_inst +def scan_list(api): """Show recently launched scans""" - key = get_api_key() - - # Get the list - api = shodan.Shodan(key) try: scans = api.scans() except shodan.APIError as e: @@ -50,11 +47,9 @@ def scan_list(): @click.option('--quiet', help='Disable the printing of information to the screen.', default=False, is_flag=True) @click.argument('port', type=int) @click.argument('protocol', type=str) -def scan_internet(quiet, port, protocol): +@get_shodan_inst +def scan_internet(quiet, port, protocol, api): """Scan the Internet for a specific port and protocol using the Shodan infrastructure.""" - key = get_api_key() - api = shodan.Shodan(key) - try: # Submit the request to Shodan click.echo('Submitting Internet scan to Shodan...', nl=False) @@ -117,10 +112,9 @@ def scan_internet(quiet, port, protocol): @scan.command(name='protocols') -def scan_protocols(): +@get_shodan_inst +def scan_protocols(api): """List the protocols that you can scan with using Shodan.""" - key = get_api_key() - api = shodan.Shodan(key) try: protocols = api.protocols() @@ -131,22 +125,22 @@ def scan_protocols(): @scan.command(name='submit') -@click.option('--wait', help='How long to wait for results to come back. If this is set to "0" or below return immediately.', default=20, type=int) +@click.option('--wait', help='How long to wait for results to come back. If this is set to "0" ' + 'or below return immediately.', default=20, type=int + ) @click.option('--filename', help='Save the results in the given file.', default='', type=str) @click.option('--force', default=False, is_flag=True) @click.option('--verbose', default=False, is_flag=True) @click.argument('netblocks', metavar='', nargs=-1) -def scan_submit(wait, filename, force, verbose, netblocks): +@get_shodan_inst +def scan_submit(wait, filename, force, verbose, netblocks, api): """Scan an IP/ netblock using Shodan.""" - key = get_api_key() - api = shodan.Shodan(key) alert = None # Submit the IPs for scanning try: # Submit the scan scan = api.scan(netblocks, force=force) - now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M') click.echo('') @@ -158,9 +152,11 @@ def scan_submit(wait, filename, force, verbose, netblocks): # Return immediately if wait <= 0: click.echo('Scan ID: {}'.format(scan['id'])) - click.echo('Exiting now, not waiting for results. Use the API or website to retrieve the results of the scan.') + click.echo('Exiting now, not waiting for results. Use the API or website to retrieve the ' + 'results of the scan.' + ) else: - # Setup an alert to wait for responses + # Set up an alert to wait for responses alert = api.create_alert('Scan: {}'.format(', '.join(netblocks)), netblocks) # Create the output file if necessary @@ -195,7 +191,8 @@ def scan_submit(wait, filename, force, verbose, netblocks): hosts[helpers.get_ip(banner)][banner['port']] = banner cache[cache_key] = True - # If we've grabbed data for more than 60 seconds it might just be a busy network and we should move on + # If we've grabbed data for more than 60 seconds it might just be a busy network + # and we should move on if time.time() - scan_start >= 60: scan = api.scan_status(scan['id']) @@ -332,10 +329,9 @@ def print_banner(banner): @scan.command(name='status') @click.argument('scan_id', type=str) -def scan_status(scan_id): +@get_shodan_inst +def scan_status(scan_id, api): """Check the status of an on-demand scan.""" - key = get_api_key() - api = shodan.Shodan(key) try: scan = api.scan_status(scan_id) click.echo(scan['status'])