Hello community, here is the log from the commit of package python-shodan for openSUSE:Factory checked in at 2019-04-05 11:56:58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-shodan (Old) and /work/SRC/openSUSE:Factory/.python-shodan.new.3908 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-shodan" Fri Apr 5 11:56:58 2019 rev:9 rq:687605 version:1.11.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-shodan/python-shodan.changes 2018-12-24 11:43:56.605316407 +0100 +++ /work/SRC/openSUSE:Factory/.python-shodan.new.3908/python-shodan.changes 2019-04-05 11:56:58.794350141 +0200 @@ -1,0 +2,15 @@ +Sat Mar 16 14:57:46 UTC 2019 - Sebastian Wagner <[email protected]> + +- update to version 1.11.1: + * Allow a single network alert to monitor multiple IP ranges (#93) +- update to version 1.11.0: + * New command **shodan scan list** to list recently launched scans + * New command **shodan alert triggers** to list the available notification triggers + * New command **shodan alert enable** to enable a notification trigger + * New command **shodan alert disable** to disable a notification trigger + * New command **shodan alert info** to show details of a specific alert + * Include timestamp, vulns and tags in CSV converter (#85) + * Fixed bug that caused an exception when parsing uncompressed data files in Python3 + * Code quality improvements + +------------------------------------------------------------------- Old: ---- shodan-1.10.4.tar.gz New: ---- shodan-1.11.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-shodan.spec ++++++ --- /var/tmp/diff_new_pack.W2sBZb/_old 2019-04-05 11:57:00.586351416 +0200 +++ /var/tmp/diff_new_pack.W2sBZb/_new 2019-04-05 11:57:00.590351419 +0200 @@ -1,7 +1,7 @@ # # spec file for package python-shodan # -# Copyright (c) 2018 SUSE LINUX GmbH, Nuernberg, Germany. +# Copyright (c) 2019 SUSE LINUX GmbH, Nuernberg, Germany. # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -19,7 +19,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %{!?license: %global license %doc} Name: python-shodan -Version: 1.10.4 +Version: 1.11.1 Release: 0 Summary: Python library and command-line utility for Shodan License: MIT ++++++ shodan-1.10.4.tar.gz -> shodan-1.11.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/.gitignore new/shodan-1.11.1/.gitignore --- old/shodan-1.10.4/.gitignore 2017-07-05 23:17:17.000000000 +0200 +++ new/shodan-1.11.1/.gitignore 2019-01-13 06:20:00.000000000 +0100 @@ -8,4 +8,6 @@ tmp/* MANIFEST .vscode/ -PKG-INFO \ No newline at end of file +PKG-INFO +venv/* +.idea/* \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/CHANGELOG.md new/shodan-1.11.1/CHANGELOG.md --- old/shodan-1.10.4/CHANGELOG.md 2018-09-22 04:14:40.000000000 +0200 +++ new/shodan-1.11.1/CHANGELOG.md 2019-02-24 10:29:28.000000000 +0100 @@ -1,6 +1,27 @@ CHANGELOG ========= +1.11.1 +------ +* Allow a single network alert to monitor multiple IP ranges (#93) + +1.11.0 +------ +* New command **shodan scan list** to list recently launched scans +* New command **shodan alert triggers** to list the available notification triggers +* New command **shodan alert enable** to enable a notification trigger +* New command **shodan alert disable** to disable a notification trigger +* New command **shodan alert info** to show details of a specific alert +* Include timestamp, vulns and tags in CSV converter (#85) +* Fixed bug that caused an exception when parsing uncompressed data files in Python3 +* Code quality improvements +* Thank you for contributions from @wagner-certat, @cclauss, @opt9, @voldmar and Antoine Neuenschwander + +1.10.4 +------ +* Fix a bug when showing old banner records that don't have the "transport" property +* Code quality improvements (bare excepts) + 1.10.3 ------ * Change bare 'except:' statements to 'except Exception:' or more specific ones diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/PKG-INFO new/shodan-1.11.1/PKG-INFO --- old/shodan-1.10.4/PKG-INFO 2018-10-05 03:00:20.000000000 +0200 +++ new/shodan-1.11.1/PKG-INFO 2019-02-24 10:58:50.000000000 +0100 @@ -1,6 +1,6 @@ -Metadata-Version: 1.1 +Metadata-Version: 2.1 Name: shodan -Version: 1.10.4 +Version: 1.11.1 Summary: Python library and command-line utility for Shodan (https://developer.shodan.io) Home-page: http://github.com/achillean/shodan-python/tree/master Author: John Matherly @@ -26,6 +26,7 @@ - `Fast/ bulk IP lookups <https://help.shodan.io/developer-fundamentals/looking-up-ip-info>`_ - Streaming API support for real-time consumption of Shodan firehose - `Network alerts (aka private firehose) <https://help.shodan.io/guides/how-to-monitor-network>`_ + - `Manage Email Notifications <https://asciinema.org/a/7WvyDtNxn0YeNU70ozsxvXDmL>`_ - Exploit search API fully implemented - Bulk data downloads - `Command-line interface <https://cli.shodan.io>`_ @@ -96,3 +97,4 @@ Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Software Development :: Libraries :: Python Modules +Description-Content-Type: text/x-rst diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/README.rst new/shodan-1.11.1/README.rst --- old/shodan-1.10.4/README.rst 2018-08-17 23:19:34.000000000 +0200 +++ new/shodan-1.11.1/README.rst 2019-02-24 10:33:19.000000000 +0100 @@ -18,6 +18,7 @@ - `Fast/ bulk IP lookups <https://help.shodan.io/developer-fundamentals/looking-up-ip-info>`_ - Streaming API support for real-time consumption of Shodan firehose - `Network alerts (aka private firehose) <https://help.shodan.io/guides/how-to-monitor-network>`_ +- `Manage Email Notifications <https://asciinema.org/a/7WvyDtNxn0YeNU70ozsxvXDmL>`_ - Exploit search API fully implemented - Bulk data downloads - `Command-line interface <https://cli.shodan.io>`_ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/setup.py new/shodan-1.11.1/setup.py --- old/shodan-1.10.4/setup.py 2018-10-05 02:49:51.000000000 +0200 +++ new/shodan-1.11.1/setup.py 2019-02-24 10:57:38.000000000 +0100 @@ -6,19 +6,19 @@ README = open('README.rst', 'r').read() setup( - name = 'shodan', - version = '1.10.4', - description = 'Python library and command-line utility for Shodan (https://developer.shodan.io)', - long_description = README, - long_description_content_type = 'text/x-rst', - author = 'John Matherly', - author_email = '[email protected]', - url = 'http://github.com/achillean/shodan-python/tree/master', - packages = ['shodan', 'shodan.cli', 'shodan.cli.converter'], - entry_points = {'console_scripts': ['shodan = shodan.__main__:main']}, - install_requires = DEPENDENCIES, - keywords = ['security', 'network'], - classifiers = [ + name='shodan', + version='1.11.1', + description='Python library and command-line utility for Shodan (https://developer.shodan.io)', + long_description=README, + long_description_content_type='text/x-rst', + author='John Matherly', + author_email='[email protected]', + url='http://github.com/achillean/shodan-python/tree/master', + packages=['shodan', 'shodan.cli', 'shodan.cli.converter'], + entry_points={'console_scripts': ['shodan=shodan.__main__:main']}, + install_requires=DEPENDENCIES, + keywords=['security', 'network'], + classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan/__main__.py new/shodan-1.11.1/shodan/__main__.py --- old/shodan-1.10.4/shodan/__main__.py 2018-10-05 02:49:11.000000000 +0200 +++ new/shodan-1.11.1/shodan/__main__.py 2019-02-11 01:22:17.000000000 +0100 @@ -49,6 +49,13 @@ from click_plugins import with_plugins from pkg_resources import iter_entry_points +# Large subcommands are stored in separate modules +from shodan.cli.alert import alert +from shodan.cli.data import data +from shodan.cli.organization import org +from shodan.cli.scan import scan + + # Make "-h" work like "--help" CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @@ -58,7 +65,6 @@ except NameError: basestring = str - # Define the main entry point for all of our commands # and expose a way for 3rd-party plugins to tie into the Shodan CLI. @with_plugins(iter_entry_points('shodan.cli.plugins')) @@ -67,11 +73,7 @@ pass -# Large subcommands are stored in separate modules -from shodan.cli.alert import alert -from shodan.cli.data import data -from shodan.cli.organization import org -from shodan.cli.scan import scan +# Setup the large subcommands main.add_command(alert) main.add_command(data) main.add_command(org) @@ -151,6 +153,7 @@ os.chmod(keyfile, 0o600) + @main.command() @click.argument('query', metavar='<search query>', nargs=-1) def count(query): @@ -203,7 +206,7 @@ try: total = api.count(query)['total'] info = api.info() - except: + except Exception: raise click.ClickException('The Shodan API is unresponsive at the moment, please try again later.') # Print some summary information about the download request @@ -275,7 +278,6 @@ raise click.ClickException(e.value) - @main.command() def info(): """Shows general information about your account""" @@ -308,7 +310,6 @@ has_filters = len(filters) > 0 - # Setup the output file handle fout = None if filename: @@ -333,7 +334,7 @@ helpers.write_banner(fout, banner) # Loop over all the fields and print the banner as a row - for field in fields: + for i, field in enumerate(fields): tmp = u'' value = get_banner_field(banner, field) if value: @@ -351,9 +352,10 @@ if color: tmp = click.style(tmp, fg=COLORIZE_FIELDS.get(field, 'white')) - # Add the field information to the row - row += tmp - row += separator + # Add the field information to the row + if i > 0: + row += separator + row += tmp click.echo(row) @@ -519,7 +521,7 @@ if len(values) > counter: has_items = True row[pos] = values[counter]['value'] - row[pos+1] = values[counter]['count'] + row[pos + 1] = values[counter]['count'] pos += 2 @@ -545,7 +547,7 @@ @click.option('--asn', help='A comma-separated list of ASNs to grab data on.', default=None, type=str) @click.option('--alert', help='The network alert ID or "all" to subscribe to all network alerts on your account.', default=None, type=str) @click.option('--compresslevel', help='The gzip compression level (0-9; 0 = no compression, 9 = most compression', default=9, type=int) -def stream(color, fields, separator, limit, datadir, ports, quiet, timeout, streamer, countries, asn, alert, compresslevel): +def stream(color, fields, separator, limit, datadir, ports, quiet, timeout, streamer, countries, asn, alert, compresslevel): """Stream data in real-time.""" # Setup the Shodan API key = get_api_key() @@ -641,9 +643,9 @@ if datadir: cur_time = timestr() if cur_time != last_time: - last_time = cur_time - fout.close() - fout = open_streaming_file(datadir, last_time) + last_time = cur_time + fout.close() + fout = open_streaming_file(datadir, last_time) helpers.write_banner(fout, banner) # Print the banner information to stdout @@ -706,7 +708,7 @@ click.echo(click.style('Not a honeypot', fg='green')) click.echo('Score: {}'.format(score)) - except: + except Exception: raise click.ClickException('Unable to calculate honeyscore') @@ -725,5 +727,6 @@ except Exception as e: raise click.ClickException(u'{}'.format(e)) + if __name__ == '__main__': main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan/alert.py new/shodan-1.11.1/shodan/alert.py --- old/shodan-1.10.4/shodan/alert.py 2015-11-20 01:41:53.000000000 +0100 +++ new/shodan-1.11.1/shodan/alert.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,9 +0,0 @@ -class Alert: - def __init__(self): - self.id = None - self.name = None - self.api_key = None - self.filters = None - self.credits = None - self.created = None - self.expires = None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan/cli/alert.py new/shodan-1.11.1/shodan/cli/alert.py --- old/shodan-1.10.4/shodan/cli/alert.py 2018-09-02 00:56:08.000000000 +0200 +++ new/shodan-1.11.1/shodan/cli/alert.py 2019-02-24 10:31:35.000000000 +0100 @@ -1,8 +1,10 @@ import click import shodan +from operator import itemgetter from shodan.cli.helpers import get_api_key + @click.group() def alert(): """Manage the network alerts for your account""" @@ -25,23 +27,61 @@ raise click.ClickException(e.value) click.echo("Alerts deleted") + @alert.command(name='create') @click.argument('name', metavar='<name>') [email protected]('netblock', metavar='<netblock>') -def alert_create(name, netblock): [email protected]('netblocks', metavar='<netblocks>', nargs=-1) +def alert_create(name, netblocks): """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, netblock) + alert = api.create_alert(name, netblocks) except shodan.APIError as e: raise click.ClickException(e.value) click.secho('Successfully created network alert!', fg='green') click.secho('Alert ID: {}'.format(alert['id']), fg='cyan') + [email protected](name='info') [email protected]('alert', metavar='<alert id>') +def alert_info(alert): + """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: + raise click.ClickException(e.value) + + click.secho(info['name'], fg='cyan') + click.secho('Created: ', nl=False, dim=True) + click.secho(info['created'], fg='magenta') + + click.secho('Notifications: ', nl=False, dim=True) + if 'triggers' in info and info['triggers']: + click.secho('enabled', fg='green') + else: + click.echo('disabled') + + click.echo('') + click.secho('Network Range(s):', dim=True) + + for network in info['filters']['ip']: + click.echo(u' > {}'.format(click.style(network, fg='yellow'))) + + click.echo('') + if 'triggers' in info and info['triggers']: + click.secho('Triggers:', dim=True) + for trigger in info['triggers']: + click.echo(u' > {}'.format(click.style(trigger, fg='yellow'))) + click.echo('') + + @alert.command(name='list') @click.option('--expired', help='Whether or not to show expired alerts.', default=True, type=bool) def alert_list(expired): @@ -57,17 +97,21 @@ if len(results) > 0: click.echo(u'# {:14} {:<21} {:<15s}'.format('Alert ID', 'Name', 'IP/ Network')) - # click.echo('#' * 65) + for alert in results: click.echo( u'{:16} {:<30} {:<35} '.format( - click.style(alert['id'], fg='yellow'), + click.style(alert['id'], fg='yellow'), click.style(alert['name'], fg='cyan'), click.style(', '.join(alert['filters']['ip']), fg='white') ), nl=False ) + if 'triggers' in alert and alert['triggers']: + click.secho('Triggers: ', fg='magenta', nl=False) + click.echo(', '.join(alert['triggers'].keys()), nl=False) + if 'expired' in alert and alert['expired']: click.secho('expired', fg='red') else: @@ -89,3 +133,68 @@ except shodan.APIError as e: raise click.ClickException(e.value) click.echo("Alert deleted") + + [email protected](name='triggers') +def alert_list_triggers(): + """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: + raise click.ClickException(e.value) + + if len(results) > 0: + click.secho('The following triggers can be enabled on alerts:', dim=True) + click.echo('') + + for trigger in sorted(results, key=itemgetter('name')): + click.secho('{:<12} '.format('Name'), dim=True, nl=False) + click.secho(trigger['name'], fg='yellow') + + click.secho('{:<12} '.format('Description'), dim=True, nl=False) + click.secho(trigger['description'], fg='cyan') + + click.secho('{:<12} '.format('Rule'), dim=True, nl=False) + click.echo(trigger['rule']) + + click.echo('') + else: + click.echo("No triggers currently available.") + + [email protected](name='enable') [email protected]('alert_id', metavar='<alert ID>') [email protected]('trigger', metavar='<trigger name>') +def alert_enable_trigger(alert_id, trigger): + """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: + raise click.ClickException(e.value) + + click.secho('Successfully enabled the trigger: {}'.format(trigger), fg='green') + + [email protected](name='disable') [email protected]('alert_id', metavar='<alert ID>') [email protected]('trigger', metavar='<trigger name>') +def alert_disable_trigger(alert_id, trigger): + """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: + raise click.ClickException(e.value) + + click.secho('Successfully disabled the trigger: {}'.format(trigger), fg='green') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan/cli/converter/__init__.py new/shodan-1.11.1/shodan/cli/converter/__init__.py --- old/shodan-1.10.4/shodan/cli/converter/__init__.py 2017-06-21 00:17:07.000000000 +0200 +++ new/shodan-1.11.1/shodan/cli/converter/__init__.py 2019-02-11 01:01:14.000000000 +0100 @@ -2,4 +2,4 @@ from .excel import ExcelConverter from .geojson import GeoJsonConverter from .images import ImagesConverter -from .kml import KmlConverter \ No newline at end of file +from .kml import KmlConverter diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan/cli/converter/base.py new/shodan-1.11.1/shodan/cli/converter/base.py --- old/shodan-1.10.4/shodan/cli/converter/base.py 2016-02-18 06:00:23.000000000 +0100 +++ new/shodan-1.11.1/shodan/cli/converter/base.py 2019-02-11 03:14:17.000000000 +0100 @@ -3,6 +3,6 @@ def __init__(self, fout): self.fout = fout - + def process(self, fout): pass diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan/cli/converter/csvc.py new/shodan-1.11.1/shodan/cli/converter/csvc.py --- old/shodan-1.10.4/shodan/cli/converter/csvc.py 2018-09-22 04:14:03.000000000 +0200 +++ new/shodan-1.11.1/shodan/cli/converter/csvc.py 2019-02-11 03:12:57.000000000 +0100 @@ -24,10 +24,13 @@ 'os', 'asn', 'port', + 'tags', + 'timestamp', 'transport', 'product', 'version', - + 'vulns', + 'ssl.cipher.version', 'ssl.cipher.bits', 'ssl.cipher.name', @@ -36,18 +39,23 @@ 'ssl.cert.serial', 'ssl.cert.fingerprint.sha1', 'ssl.cert.fingerprint.sha256', - + 'html', 'title', ] - + def process(self, files): writer = csv_writer(self.fout, dialect=excel) - + # Write the header writer.writerow(self.fields) - + for banner in iterate_files(files): + # The "vulns" property can't be nicely flattened as-is so we turn + # it into a list before processing the banner. + if 'vulns' in banner: + banner['vulns'] = banner['vulns'].keys() + try: row = [] for field in self.fields: @@ -56,33 +64,32 @@ writer.writerow(row) except Exception: pass - + def banner_field(self, banner, flat_field): # The provided field is a collapsed form of the actual field fields = flat_field.split('.') - + try: current_obj = banner for field in fields: current_obj = current_obj[field] - + # Convert a list into a concatenated string if isinstance(current_obj, list): current_obj = ','.join([str(i) for i in current_obj]) - + return current_obj except Exception: pass - + return '' - + def flatten(self, d, parent_key='', sep='.'): items = [] for k, v in d.items(): new_key = parent_key + sep + k if parent_key else k if isinstance(v, MutableMapping): - # pylint: disable=E0602 - items.extend(flatten(v, new_key, sep=sep).items()) + items.extend(self.flatten(v, new_key, sep=sep).items()) else: items.append((new_key, v)) return dict(items) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan/cli/converter/excel.py new/shodan-1.11.1/shodan/cli/converter/excel.py --- old/shodan-1.10.4/shodan/cli/converter/excel.py 2018-09-22 04:14:03.000000000 +0200 +++ new/shodan-1.11.1/shodan/cli/converter/excel.py 2019-02-11 03:14:03.000000000 +0100 @@ -23,7 +23,7 @@ 'transport', 'product', 'version', - + 'http.server', 'http.title', ] @@ -40,7 +40,7 @@ 'http.server': 'Web Server', 'http.title': 'Website Title', } - + def process(self, files): # Get the filename from the already-open file handle filename = self.fout.name @@ -55,14 +55,14 @@ bold = workbook.add_format({ 'bold': 1, }) - + # Create the main worksheet where all the raw data is shown main_sheet = workbook.add_worksheet('Raw Data') # Write the header - main_sheet.write(0, 0, 'IP', bold) # The IP field can be either ip_str or ipv6 so we treat it differently + main_sheet.write(0, 0, 'IP', bold) # The IP field can be either ip_str or ipv6 so we treat it differently main_sheet.set_column(0, 0, 20) - + row = 0 col = 1 for field in self.fields: @@ -80,7 +80,7 @@ for field in self.fields: value = self.banner_field(banner, field) data.append(value) - + # Write those values to the main workbook # Starting off w/ the special "IP" property main_sheet.write_string(row, 0, get_ip(banner)) @@ -92,11 +92,11 @@ row += 1 except Exception: pass - + # Aggregate summary information total += 1 ports[banner['port']] += 1 - + summary_sheet = workbook.add_worksheet('Summary') summary_sheet.write(0, 0, 'Total', bold) summary_sheet.write(0, 1, total) @@ -109,22 +109,22 @@ summary_sheet.write(row, col, key) summary_sheet.write(row, col + 1, value) row += 1 - + def banner_field(self, banner, flat_field): # The provided field is a collapsed form of the actual field fields = flat_field.split('.') - + try: current_obj = banner for field in fields: current_obj = current_obj[field] - + # Convert a list into a concatenated string if isinstance(current_obj, list): current_obj = ','.join([str(i) for i in current_obj]) - + return current_obj except Exception: pass - + return '' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan/cli/converter/geojson.py new/shodan-1.11.1/shodan/cli/converter/geojson.py --- old/shodan-1.10.4/shodan/cli/converter/geojson.py 2018-09-22 04:14:03.000000000 +0200 +++ new/shodan-1.11.1/shodan/cli/converter/geojson.py 2019-02-11 03:19:42.000000000 +0100 @@ -2,39 +2,39 @@ from .base import Converter from ...helpers import get_ip, iterate_files + class GeoJsonConverter(Converter): - + def header(self): self.fout.write("""{ "type": "FeatureCollection", "features": [ """) - + def footer(self): self.fout.write("""{ }]}""") - + def process(self, files): # Write the header self.header() - + hosts = {} for banner in iterate_files(files): ip = get_ip(banner) if not ip: continue - + if ip not in hosts: hosts[ip] = banner hosts[ip]['ports'] = [] - + hosts[ip]['ports'].append(banner['port']) - + for ip, host in iter(hosts.items()): self.write(host) - + self.footer() - - + def write(self, host): try: ip = get_ip(host) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan/cli/converter/images.py new/shodan-1.11.1/shodan/cli/converter/images.py --- old/shodan-1.10.4/shodan/cli/converter/images.py 2017-06-21 01:01:02.000000000 +0200 +++ new/shodan-1.11.1/shodan/cli/converter/images.py 2019-02-11 03:14:34.000000000 +0100 @@ -14,7 +14,7 @@ # special code in the Shodan CLI that relies on the "dirname" property to let # the user know where the images have been stored. dirname = None - + def process(self, files): # Get the filename from the already-open file handle and use it as # the directory name to store the images. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan/cli/converter/kml.py new/shodan-1.11.1/shodan/cli/converter/kml.py --- old/shodan-1.10.4/shodan/cli/converter/kml.py 2018-09-22 04:14:03.000000000 +0200 +++ new/shodan-1.11.1/shodan/cli/converter/kml.py 2019-02-11 03:19:06.000000000 +0100 @@ -2,38 +2,38 @@ from .base import Converter from ...helpers import iterate_files + class KmlConverter(Converter): - + def header(self): self.fout.write("""<?xml version="1.0" encoding="UTF-8"?> <kml xmlns="http://www.opengis.net/kml/2.2"> <Document>""") - + def footer(self): self.fout.write("""</Document></kml>""") - + def process(self, files): # Write the header self.header() - + hosts = {} for banner in iterate_files(files): ip = banner.get('ip_str', banner.get('ipv6', None)) if not ip: continue - + if ip not in hosts: hosts[ip] = banner hosts[ip]['ports'] = [] - + hosts[ip]['ports'].append(banner['port']) - + for ip, host in iter(hosts.items()): self.write(host) - + self.footer() - - + def write(self, host): try: ip = host.get('ip_str', host.get('ipv6', None)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan/cli/helpers.py new/shodan-1.11.1/shodan/cli/helpers.py --- old/shodan-1.10.4/shodan/cli/helpers.py 2018-09-22 04:14:03.000000000 +0200 +++ new/shodan-1.11.1/shodan/cli/helpers.py 2019-02-11 01:45:35.000000000 +0100 @@ -10,6 +10,12 @@ from .settings import SHODAN_CONFIG_DIR +try: + basestring # Python 2 +except NameError: + 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) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan/cli/host.py new/shodan-1.11.1/shodan/cli/host.py --- old/shodan-1.10.4/shodan/cli/host.py 2018-10-05 01:20:34.000000000 +0200 +++ new/shodan-1.11.1/shodan/cli/host.py 2019-02-11 01:46:29.000000000 +0100 @@ -64,9 +64,9 @@ for port in ports: banner = { 'port': port, - 'transport': 'tcp', # All the filtered services use TCP - 'timestamp': host['data'][-1]['timestamp'], # Use the timestamp of the oldest banner - 'placeholder': True, # Don't store this banner when the file is saved + 'transport': 'tcp', # All the filtered services use TCP + 'timestamp': host['data'][-1]['timestamp'], # Use the timestamp of the oldest banner + 'placeholder': True, # Don't store this banner when the file is saved } host['data'].append(banner) @@ -94,7 +94,7 @@ # Show optional ssl info if 'ssl' in banner: if 'versions' in banner['ssl'] and banner['ssl']['versions']: - click.echo('\t|-- SSL Versions: {}'.format(', '.join([version for version in sorted(banner['ssl']['versions']) if not version.startswith('-')]))) + click.echo('\t|-- SSL Versions: {}'.format(', '.join([item for item in sorted(banner['ssl']['versions']) if not version.startswith('-')]))) if 'dhparams' in banner['ssl'] and banner['ssl']['dhparams']: click.echo('\t|-- Diffie-Hellman Parameters:') click.echo('\t\t{:15s}{}\n\t\t{:15s}{}'.format('Bits:', banner['ssl']['dhparams']['bits'], 'Generator:', banner['ssl']['dhparams']['generator'])) @@ -119,4 +119,4 @@ HOST_PRINT = { 'pretty': host_print_pretty, 'tsv': host_print_tsv, -} \ No newline at end of file +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan/cli/organization.py new/shodan-1.11.1/shodan/cli/organization.py --- old/shodan-1.10.4/shodan/cli/organization.py 2018-09-22 04:14:03.000000000 +0200 +++ new/shodan-1.11.1/shodan/cli/organization.py 2019-02-11 01:45:19.000000000 +0100 @@ -22,7 +22,7 @@ api.org.add_member(user, notify=not silent) except shodan.APIError as e: raise click.ClickException(e.value) - + click.secho('Successfully added the new member', fg='green') @@ -39,11 +39,11 @@ click.secho(organization['name'], fg='cyan') click.secho('Access Level: ', nl=False, dim=True) click.secho(humanize_api_plan(organization['upgrade_type']), fg='magenta') - + if organization['domains']: click.secho('Authorized Domains: ', nl=False, dim=True) click.echo(', '.join(organization['domains'])) - + click.echo('') click.secho('Administrators:', dim=True) @@ -51,8 +51,8 @@ click.echo(u' > {:30}\t{:30}'.format( click.style(admin['username'], fg='yellow'), admin['email']) - ) - + ) + click.echo('') if organization['members']: click.secho('Members:', dim=True) @@ -76,5 +76,5 @@ api.org.remove_member(user) except shodan.APIError as e: raise click.ClickException(e.value) - + click.secho('Successfully removed the member', fg='green') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan/cli/scan.py new/shodan-1.11.1/shodan/cli/scan.py --- old/shodan-1.10.4/shodan/cli/scan.py 2018-08-31 02:54:21.000000000 +0200 +++ new/shodan-1.11.1/shodan/cli/scan.py 2019-02-11 03:11:51.000000000 +0100 @@ -17,6 +17,35 @@ pass [email protected](name='list') +def scan_list(): + """Show recently launched scans""" + key = get_api_key() + + # Get the list + api = shodan.Shodan(key) + try: + scans = api.scans() + except shodan.APIError as e: + raise click.ClickException(e.value) + + if len(scans) > 0: + click.echo(u'# {} Scans Total - Showing 10 most recent scans:'.format(scans['total'])) + click.echo(u'# {:20} {:<15} {:<10} {:<15s}'.format('Scan ID', 'Status', 'Size', 'Timestamp')) + # click.echo('#' * 65) + for scan in scans['matches'][:10]: + click.echo( + u'{:31} {:<24} {:<10} {:<15s}'.format( + click.style(scan['id'], fg='yellow'), + click.style(scan['status'], fg='cyan'), + scan['size'], + scan['created'] + ) + ) + else: + click.echo("You haven't yet launched any scans.") + + @scan.command(name='internet') @click.option('--quiet', help='Disable the printing of information to the screen.', default=False, is_flag=True) @click.argument('port', type=int) @@ -58,10 +87,9 @@ if not quiet: click.echo('{0:<40} {1:<20} {2}'.format( - click.style(helpers.get_ip(banner), fg=COLORIZE_FIELDS['ip_str']), - click.style(str(banner['port']), fg=COLORIZE_FIELDS['port']), - ';'.join(banner['hostnames']) - ) + click.style(helpers.get_ip(banner), fg=COLORIZE_FIELDS['ip_str']), + click.style(str(banner['port']), fg=COLORIZE_FIELDS['port']), + ';'.join(banner['hostnames'])) ) except shodan.APIError as e: # We stop waiting for results if the scan has been processed by the crawlers and diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan/cli/worldmap.py new/shodan-1.11.1/shodan/cli/worldmap.py --- old/shodan-1.10.4/shodan/cli/worldmap.py 2018-09-22 04:14:03.000000000 +0200 +++ new/shodan-1.11.1/shodan/cli/worldmap.py 2019-02-11 01:44:09.000000000 +0100 @@ -108,14 +108,14 @@ TODO: filter out stuff that doesn't fit TODO: make it possible to use "zoomed" maps """ - width = (self.corners[3]-self.corners[1]) - height = (self.corners[2]-self.corners[0]) + width = (self.corners[3] - self.corners[1]) + height = (self.corners[2] - self.corners[0]) # change to 0-180, 0-360 - abs_lat = -lat+90 - abs_lon = lon+180 - x = (abs_lon/360.0)*width + self.corners[1] - y = (abs_lat/180.0)*height + self.corners[0] + abs_lat = -lat + 90 + abs_lon = lon + 180 + x = (abs_lon / 360.0) * width + self.corners[1] + y = (abs_lat / 180.0) * height + self.corners[0] return int(x), int(y) def set_data(self, data): @@ -155,7 +155,7 @@ self.window.addstr(0, 0, self.map) # FIXME: position to be defined in map config? - row = self.corners[2]-6 + row = self.corners[2] - 6 items_to_show = 5 for lat, lon, char, desc, attrs, color in self.data: # to make this work almost everywhere. see http://docs.python.org/2/library/curses.html @@ -177,7 +177,7 @@ self.window.addstr(row, 1, det_show, attrs) row += 1 items_to_show -= 1 - except StandardError: + except Exception: # FIXME: check window size before addstr() break self.window.overwrite(target) @@ -257,6 +257,7 @@ api = Shodan(get_api_key()) return launch_map(api) + if __name__ == '__main__': import sys sys.exit(main()) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan/client.py new/shodan-1.11.1/shodan/client.py --- old/shodan-1.10.4/shodan/client.py 2018-09-22 04:14:03.000000000 +0200 +++ new/shodan-1.11.1/shodan/client.py 2019-02-11 01:04:51.000000000 +0100 @@ -176,7 +176,7 @@ :param key: The Shodan API key. :type key: str :param proxies: A proxies array for the requests library, e.g. {'https': 'your proxy'} - :type key: dict + :type proxies: dict """ self.api_key = key self.base_url = 'https://api.shodan.io' @@ -347,6 +347,16 @@ return self._request('/shodan/scan', params, method='post') + def scans(self, page=1): + """Get a list of scans submitted + + :param page: Page through the list of scans 100 results at a time + :type page: int + """ + return self._request('/shodan/scans', { + 'page': page, + }) + def scan_internet(self, port, protocol): """Scan a network using Shodan @@ -438,7 +448,7 @@ try: yield banner except GeneratorExit: - return # exit out of the function + return # exit out of the function page += 1 tries = 0 except Exception: @@ -447,7 +457,7 @@ break tries += 1 - time.sleep(1.0) # wait 1 second if the search errored out for some reason + time.sleep(1.0) # wait 1 second if the search errored out for some reason def search_tokens(self, query): """Returns information about the search query itself (filters used etc.) @@ -507,8 +517,8 @@ def queries_tags(self, size=10): """Search the directory of saved search queries in Shodan. - :param query: The number of tags to return - :type page: int + :param size: The number of tags to return + :type size: int :returns: A list of tags. """ @@ -518,12 +528,14 @@ return self._request('/shodan/query/tags', args) def create_alert(self, name, ip, expires=0): - """Search the directory of saved search queries in Shodan. + """Create a network alert/ private firehose for the specified IP range(s) - :param query: The number of tags to return - :type page: int + :param name: Name of the alert + :type name: str + :param ip: Network range(s) to monitor + :type ip: str OR list of str - :returns: A list of tags. + :returns: A dict describing the alert """ data = { 'name': name, @@ -547,8 +559,7 @@ response = api_request(self.api_key, func, params={ 'include_expired': include_expired, - }, - proxies=self._session.proxies) + }, proxies=self._session.proxies) return response @@ -561,3 +572,17 @@ return response + def alert_triggers(self): + """Return a list of available triggers that can be enabled for alerts. + + :returns: A list of triggers + """ + return self._request('/shodan/alert/triggers', {}) + + def enable_alert_trigger(self, aid, trigger): + """Enable the given trigger on the alert.""" + return self._request('/shodan/alert/{}/trigger/{}'.format(aid, trigger), {}, method='put') + + def disable_alert_trigger(self, aid, trigger): + """Disable the given trigger on the alert.""" + return self._request('/shodan/alert/{}/trigger/{}'.format(aid, trigger), {}, method='delete') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan/exception.py new/shodan-1.11.1/shodan/exception.py --- old/shodan-1.10.4/shodan/exception.py 2018-08-18 03:27:55.000000000 +0200 +++ new/shodan-1.11.1/shodan/exception.py 2019-02-11 01:35:57.000000000 +0100 @@ -2,11 +2,10 @@ """This exception gets raised whenever a non-200 status code was returned by the Shodan API.""" def __init__(self, value): self.value = value - + def __str__(self): return self.value class APITimeout(APIError): - pass - + pass diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan/helpers.py new/shodan-1.11.1/shodan/helpers.py --- old/shodan-1.10.4/shodan/helpers.py 2018-09-23 03:58:44.000000000 +0200 +++ new/shodan-1.11.1/shodan/helpers.py 2019-02-16 03:46:04.000000000 +0100 @@ -19,7 +19,7 @@ if isinstance(facet, basestring): facet_str += facet else: - facet_str += '%s:%s' % (facet[0], facet[1]) + facet_str += '{}:{}'.format(facet[0], facet[1]) facet_str += ',' return facet_str[:-1] @@ -76,7 +76,7 @@ # Parse the text into JSON try: data = data.json() - except: + except Exception: raise APIError('Unable to parse JSON response') # Raise an exception if an error occurred @@ -113,12 +113,14 @@ for line in fin: # Ensure the line has been decoded into a string to prevent errors w/ Python3 - line = line.decode('utf-8') + if not isinstance(line, basestring): + line = line.decode('utf-8') # Convert the JSON into a native Python object banner = loads(line) yield banner + def get_screenshot(banner): if 'opts' in banner and 'screenshot' in banner['opts']: return banner['opts']['screenshot'] @@ -159,14 +161,13 @@ >>> humanize_bytes(1024*1234*1111,1) '1.3 GB' """ - if bytes == 1: return '1 byte' if bytes < 1024: return '%.*f %s' % (precision, bytes, "bytes") suffixes = ['KB', 'MB', 'GB', 'TB', 'PB'] - multiple = 1024.0 #.0 force float on python 2 + multiple = 1024.0 # .0 to force float on python 2 for suffix in suffixes: bytes /= multiple if bytes < multiple: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan/stream.py new/shodan-1.11.1/shodan/stream.py --- old/shodan-1.10.4/shodan/stream.py 2018-08-01 08:07:51.000000000 +0200 +++ new/shodan-1.11.1/shodan/stream.py 2019-02-11 01:05:46.000000000 +0100 @@ -21,7 +21,7 @@ # The user doesn't want to use a timeout # If the timeout is specified as 0 then we also don't want to have a timeout - if ( timeout and timeout <= 0 ) or ( timeout == 0 ): + if (timeout and timeout <= 0) or (timeout == 0): timeout = None # If the user requested a timeout then we need to disable heartbeat messages @@ -43,16 +43,16 @@ # not specific to Cloudflare. if req.status_code != 524 or timeout >= 0: break - except Exception as e: + except Exception: raise APIError('Unable to contact the Shodan Streaming API') if req.status_code != 200: try: data = json.loads(req.text) raise APIError(data['error']) - except APIError as e: + except APIError: raise - except Exception as e: + except Exception: pass raise APIError('Invalid API key or you do not have access to the Streaming API') if req.encoding is None: @@ -78,9 +78,9 @@ try: for line in self._iter_stream(stream, raw): yield line - except requests.exceptions.ConnectionError as e: + except requests.exceptions.ConnectionError: raise APIError('Stream timed out') - except ssl.SSLError as e: + except ssl.SSLError: raise APIError('Stream timed out') def asn(self, asn, raw=False, timeout=None): @@ -123,4 +123,3 @@ stream = self._create_stream('/shodan/ports/%s' % ','.join([str(port) for port in ports]), timeout=timeout) for line in self._iter_stream(stream, raw): yield line - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan/threatnet.py new/shodan-1.11.1/shodan/threatnet.py --- old/shodan-1.10.4/shodan/threatnet.py 2018-08-31 03:40:23.000000000 +0200 +++ new/shodan-1.11.1/shodan/threatnet.py 2019-02-11 01:18:29.000000000 +0100 @@ -24,13 +24,13 @@ try: req = requests.get(self.base_url + name, params={'key': self.parent.api_key}, stream=True, proxies=self.proxies) - except: + except Exception: raise APIError('Unable to contact the Shodan Streaming API') if req.status_code != 200: try: raise APIError(req.json()['error']) - except: + except Exception: pass raise APIError('Invalid API key or you do not have access to the Streaming API') return req @@ -65,4 +65,3 @@ self.api_key = key self.base_url = 'https://api.shodan.io' self.stream = self.Stream(self) - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan.egg-info/PKG-INFO new/shodan-1.11.1/shodan.egg-info/PKG-INFO --- old/shodan-1.10.4/shodan.egg-info/PKG-INFO 2018-10-05 03:00:20.000000000 +0200 +++ new/shodan-1.11.1/shodan.egg-info/PKG-INFO 2019-02-24 10:58:50.000000000 +0100 @@ -1,6 +1,6 @@ -Metadata-Version: 1.1 +Metadata-Version: 2.1 Name: shodan -Version: 1.10.4 +Version: 1.11.1 Summary: Python library and command-line utility for Shodan (https://developer.shodan.io) Home-page: http://github.com/achillean/shodan-python/tree/master Author: John Matherly @@ -26,6 +26,7 @@ - `Fast/ bulk IP lookups <https://help.shodan.io/developer-fundamentals/looking-up-ip-info>`_ - Streaming API support for real-time consumption of Shodan firehose - `Network alerts (aka private firehose) <https://help.shodan.io/guides/how-to-monitor-network>`_ + - `Manage Email Notifications <https://asciinema.org/a/7WvyDtNxn0YeNU70ozsxvXDmL>`_ - Exploit search API fully implemented - Bulk data downloads - `Command-line interface <https://cli.shodan.io>`_ @@ -96,3 +97,4 @@ Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Software Development :: Libraries :: Python Modules +Description-Content-Type: text/x-rst diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan.egg-info/SOURCES.txt new/shodan-1.11.1/shodan.egg-info/SOURCES.txt --- old/shodan-1.10.4/shodan.egg-info/SOURCES.txt 2018-10-05 03:00:20.000000000 +0200 +++ new/shodan-1.11.1/shodan.egg-info/SOURCES.txt 2019-02-24 10:58:50.000000000 +0100 @@ -18,7 +18,6 @@ docs/examples/query-summary.rst shodan/__init__.py shodan/__main__.py -shodan/alert.py shodan/client.py shodan/exception.py shodan/helpers.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/shodan.egg-info/requires.txt new/shodan-1.11.1/shodan.egg-info/requires.txt --- old/shodan-1.10.4/shodan.egg-info/requires.txt 2018-10-05 03:00:20.000000000 +0200 +++ new/shodan-1.11.1/shodan.egg-info/requires.txt 2019-02-24 10:58:50.000000000 +0100 @@ -1,5 +1,5 @@ -XlsxWriter click click-plugins colorama requests>=2.2.1 +XlsxWriter diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/shodan-1.10.4/tests/test_shodan.py new/shodan-1.11.1/tests/test_shodan.py --- old/shodan-1.10.4/tests/test_shodan.py 2018-09-22 04:14:03.000000000 +0200 +++ new/shodan-1.11.1/tests/test_shodan.py 2019-02-11 00:53:38.000000000 +0100 @@ -9,148 +9,148 @@ class ShodanTests(unittest.TestCase): - api = None - FACETS = [ - 'port', - ('domain', 1) - ] - QUERIES = { - 'simple': 'cisco-ios', - 'minify': 'apache', - 'advanced': 'apache port:443', - 'empty': 'asdasdasdasdasdasdasdasdasdhjihjkjk', - } - - def setUp(self): - self.api = shodan.Shodan(open('SHODAN-API-KEY').read().strip()) - - def test_search_simple(self): - results = self.api.search(self.QUERIES['simple']) - - # Make sure the properties exist - self.assertIn('matches', results) - self.assertIn('total', results) - - # Make sure no error occurred - self.assertNotIn('error', results) - - # Make sure some values were returned - self.assertTrue(results['matches']) - self.assertTrue(results['total']) - - # A regular search shouldn't have the optional info - self.assertNotIn('opts', results['matches'][0]) - - def test_search_empty(self): - results = self.api.search(self.QUERIES['empty']) - self.assertTrue(len(results['matches']) == 0) - self.assertEqual(results['total'], 0) - - def test_search_facets(self): - results = self.api.search(self.QUERIES['simple'], facets=self.FACETS) - - self.assertTrue(results['facets']['port']) - self.assertEqual(len(results['facets']['domain']), 1) - - def test_count_simple(self): - results = self.api.count(self.QUERIES['simple']) - - # Make sure the properties exist - self.assertIn('matches', results) - self.assertIn('total', results) - - # Make sure no error occurred - self.assertNotIn('error', results) - - # Make sure no values were returned - self.assertFalse(results['matches']) - self.assertTrue(results['total']) - - def test_count_facets(self): - results = self.api.count(self.QUERIES['simple'], facets=self.FACETS) - - self.assertTrue(results['facets']['port']) - self.assertEqual(len(results['facets']['domain']), 1) - - def test_host_details(self): - host = self.api.host('147.228.101.7') - - self.assertEqual('147.228.101.7', host['ip_str']) - self.assertFalse(isinstance(host['ip'], basestring)) - - def test_search_minify(self): - results = self.api.search(self.QUERIES['minify'], minify=False) - self.assertIn('opts', results['matches'][0]) - - def test_exploits_search(self): - results = self.api.exploits.search('apache') - self.assertIn('matches', results) - self.assertIn('total', results) - self.assertTrue(results['matches']) - - def test_exploits_search_paging(self): - results = self.api.exploits.search('apache', page=1) - match1 = results['matches'][0] - results = self.api.exploits.search('apache', page=2) - match2 = results['matches'][0] - - self.assertNotEqual(match1['_id'], match2['_id']) - - def test_exploits_search_facets(self): - results = self.api.exploits.search('apache', facets=['source', ('author', 1)]) - self.assertIn('facets', results) - self.assertTrue(results['facets']['source']) - self.assertTrue(len(results['facets']['author']) == 1) - - def test_exploits_count(self): - results = self.api.exploits.count('apache') - self.assertIn('matches', results) - self.assertIn('total', results) - self.assertTrue(len(results['matches']) == 0) - - def test_exploits_count_facets(self): - results = self.api.exploits.count('apache', facets=['source', ('author', 1)]) - self.assertEqual(len(results['matches']), 0) - self.assertIn('facets', results) - self.assertTrue(results['facets']['source']) - self.assertTrue(len(results['facets']['author']) == 1) - - # Test error responses - def test_invalid_key(self): - api = shodan.Shodan('garbage') - raised = False - try: - api.search('something') - except shodan.APIError as e: - raised = True - - self.assertTrue(raised) - - def test_invalid_host_ip(self): - raised = False - try: - host = self.api.host('test') - except shodan.APIError as e: - raised = True - - self.assertTrue(raised) - - def test_search_empty_query(self): - raised = False - try: - self.api.search('') - except shodan.APIError as e: - raised = True - self.assertTrue(raised) - - def test_search_advanced_query(self): - # The free API plan can't use filters - raised = False - try: - self.api.search(self.QUERIES['advanced']) - except shodan.APIError as e: - raised = True - self.assertTrue(raised) + api = None + FACETS = [ + 'port', + ('domain', 1) + ] + QUERIES = { + 'simple': 'cisco-ios', + 'minify': 'apache', + 'advanced': 'apache port:443', + 'empty': 'asdasdasdasdasdasdasdasdasdhjihjkjk', + } + + def setUp(self): + self.api = shodan.Shodan(open('SHODAN-API-KEY').read().strip()) + + def test_search_simple(self): + results = self.api.search(self.QUERIES['simple']) + + # Make sure the properties exist + self.assertIn('matches', results) + self.assertIn('total', results) + + # Make sure no error occurred + self.assertNotIn('error', results) + + # Make sure some values were returned + self.assertTrue(results['matches']) + self.assertTrue(results['total']) + + # A regular search shouldn't have the optional info + self.assertNotIn('opts', results['matches'][0]) + + def test_search_empty(self): + results = self.api.search(self.QUERIES['empty']) + self.assertTrue(len(results['matches']) == 0) + self.assertEqual(results['total'], 0) + + def test_search_facets(self): + results = self.api.search(self.QUERIES['simple'], facets=self.FACETS) + + self.assertTrue(results['facets']['port']) + self.assertEqual(len(results['facets']['domain']), 1) + + def test_count_simple(self): + results = self.api.count(self.QUERIES['simple']) + + # Make sure the properties exist + self.assertIn('matches', results) + self.assertIn('total', results) + + # Make sure no error occurred + self.assertNotIn('error', results) + + # Make sure no values were returned + self.assertFalse(results['matches']) + self.assertTrue(results['total']) + + def test_count_facets(self): + results = self.api.count(self.QUERIES['simple'], facets=self.FACETS) + + self.assertTrue(results['facets']['port']) + self.assertEqual(len(results['facets']['domain']), 1) + + def test_host_details(self): + host = self.api.host('147.228.101.7') + + self.assertEqual('147.228.101.7', host['ip_str']) + self.assertFalse(isinstance(host['ip'], basestring)) + + def test_search_minify(self): + results = self.api.search(self.QUERIES['minify'], minify=False) + self.assertIn('opts', results['matches'][0]) + + def test_exploits_search(self): + results = self.api.exploits.search('apache') + self.assertIn('matches', results) + self.assertIn('total', results) + self.assertTrue(results['matches']) + + def test_exploits_search_paging(self): + results = self.api.exploits.search('apache', page=1) + match1 = results['matches'][0] + results = self.api.exploits.search('apache', page=2) + match2 = results['matches'][0] + + self.assertNotEqual(match1['_id'], match2['_id']) + + def test_exploits_search_facets(self): + results = self.api.exploits.search('apache', facets=['source', ('author', 1)]) + self.assertIn('facets', results) + self.assertTrue(results['facets']['source']) + self.assertTrue(len(results['facets']['author']) == 1) + + def test_exploits_count(self): + results = self.api.exploits.count('apache') + self.assertIn('matches', results) + self.assertIn('total', results) + self.assertTrue(len(results['matches']) == 0) + + def test_exploits_count_facets(self): + results = self.api.exploits.count('apache', facets=['source', ('author', 1)]) + self.assertEqual(len(results['matches']), 0) + self.assertIn('facets', results) + self.assertTrue(results['facets']['source']) + self.assertTrue(len(results['facets']['author']) == 1) + + # Test error responses + def test_invalid_key(self): + api = shodan.Shodan('garbage') + raised = False + try: + api.search('something') + except shodan.APIError: + raised = True + + self.assertTrue(raised) + + def test_invalid_host_ip(self): + raised = False + try: + self.api.host('test') + except shodan.APIError: + raised = True + + self.assertTrue(raised) + + def test_search_empty_query(self): + raised = False + try: + self.api.search('') + except shodan.APIError: + raised = True + self.assertTrue(raised) + + def test_search_advanced_query(self): + # The free API plan can't use filters + raised = False + try: + self.api.search(self.QUERIES['advanced']) + except shodan.APIError: + raised = True + self.assertTrue(raised) if __name__ == '__main__':
