Codebase list python-shodan / 2d8619f
New upstream version 1.11.0 Samuel Henrique 5 years ago
25 changed file(s) with 460 addition(s) and 259 deletion(s). Raw diff Collapse all Expand all
77 tmp/*
88 MANIFEST
99 .vscode/
10 PKG-INFO
10 PKG-INFO
11 venv/*
12 .idea/*
00 CHANGELOG
11 =========
2
3 1.11.0
4 ----------
5 * New command **shodan scan list** to list recently launched scans
6 * New command **shodan alert triggers** to list the available notification triggers
7 * New command **shodan alert enable** to enable a notification trigger
8 * New command **shodan alert disable** to disable a notification trigger
9 * New command **shodan alert info** to show details of a specific alert
10 * Include timestamp, vulns and tags in CSV converter (#85)
11 * Fixed bug that caused an exception when parsing uncompressed data files in Python3
12 * Code quality improvements
13 * Thank you for contributions from @wagner-certat, @cclauss, @opt9, @voldmar and Antoine Neuenschwander
14
15 1.10.4
16 ------
17 * Fix a bug when showing old banner records that don't have the "transport" property
18 * Code quality improvements (bare excepts)
219
320 1.10.3
421 ------
55 README = open('README.rst', 'r').read()
66
77 setup(
8 name = 'shodan',
9 version = '1.10.4',
10 description = 'Python library and command-line utility for Shodan (https://developer.shodan.io)',
11 long_description = README,
12 long_description_content_type = 'text/x-rst',
13 author = 'John Matherly',
14 author_email = 'jmath@shodan.io',
15 url = 'http://github.com/achillean/shodan-python/tree/master',
16 packages = ['shodan', 'shodan.cli', 'shodan.cli.converter'],
17 entry_points = {'console_scripts': ['shodan = shodan.__main__:main']},
18 install_requires = DEPENDENCIES,
19 keywords = ['security', 'network'],
20 classifiers = [
8 name='shodan',
9 version='1.11.0',
10 description='Python library and command-line utility for Shodan (https://developer.shodan.io)',
11 long_description=README,
12 long_description_content_type='text/x-rst',
13 author='John Matherly',
14 author_email='jmath@shodan.io',
15 url='http://github.com/achillean/shodan-python/tree/master',
16 packages=['shodan', 'shodan.cli', 'shodan.cli.converter'],
17 entry_points={'console_scripts': ['shodan=shodan.__main__:main']},
18 install_requires=DEPENDENCIES,
19 keywords=['security', 'network'],
20 classifiers=[
2121 'Development Status :: 5 - Production/Stable',
2222 'Intended Audience :: Developers',
2323 'License :: OSI Approved :: MIT License',
4848 from click_plugins import with_plugins
4949 from pkg_resources import iter_entry_points
5050
51 # Large subcommands are stored in separate modules
52 from shodan.cli.alert import alert
53 from shodan.cli.data import data
54 from shodan.cli.organization import org
55 from shodan.cli.scan import scan
56
57
5158 # Make "-h" work like "--help"
5259 CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
5360
5663 basestring
5764 except NameError:
5865 basestring = str
59
6066
6167 # Define the main entry point for all of our commands
6268 # and expose a way for 3rd-party plugins to tie into the Shodan CLI.
6672 pass
6773
6874
69 # Large subcommands are stored in separate modules
70 from shodan.cli.alert import alert
71 from shodan.cli.data import data
72 from shodan.cli.organization import org
73 from shodan.cli.scan import scan
75 # Setup the large subcommands
7476 main.add_command(alert)
7577 main.add_command(data)
7678 main.add_command(org)
150152
151153 os.chmod(keyfile, 0o600)
152154
155
153156 @main.command()
154157 @click.argument('query', metavar='<search query>', nargs=-1)
155158 def count(query):
202205 try:
203206 total = api.count(query)['total']
204207 info = api.info()
205 except:
208 except Exception:
206209 raise click.ClickException('The Shodan API is unresponsive at the moment, please try again later.')
207210
208211 # Print some summary information about the download request
272275 helpers.write_banner(fout, banner)
273276 except shodan.APIError as e:
274277 raise click.ClickException(e.value)
275
276278
277279
278280 @main.command()
307309
308310 has_filters = len(filters) > 0
309311
310
311312 # Setup the output file handle
312313 fout = None
313314 if filename:
332333 helpers.write_banner(fout, banner)
333334
334335 # Loop over all the fields and print the banner as a row
335 for field in fields:
336 for i, field in enumerate(fields):
336337 tmp = u''
337338 value = get_banner_field(banner, field)
338339 if value:
350351 if color:
351352 tmp = click.style(tmp, fg=COLORIZE_FIELDS.get(field, 'white'))
352353
353 # Add the field information to the row
354 row += tmp
355 row += separator
354 # Add the field information to the row
355 if i > 0:
356 row += separator
357 row += tmp
356358
357359 click.echo(row)
358360
518520 if len(values) > counter:
519521 has_items = True
520522 row[pos] = values[counter]['value']
521 row[pos+1] = values[counter]['count']
523 row[pos + 1] = values[counter]['count']
522524
523525 pos += 2
524526
544546 @click.option('--asn', help='A comma-separated list of ASNs to grab data on.', default=None, type=str)
545547 @click.option('--alert', help='The network alert ID or "all" to subscribe to all network alerts on your account.', default=None, type=str)
546548 @click.option('--compresslevel', help='The gzip compression level (0-9; 0 = no compression, 9 = most compression', default=9, type=int)
547 def stream(color, fields, separator, limit, datadir, ports, quiet, timeout, streamer, countries, asn, alert, compresslevel):
549 def stream(color, fields, separator, limit, datadir, ports, quiet, timeout, streamer, countries, asn, alert, compresslevel):
548550 """Stream data in real-time."""
549551 # Setup the Shodan API
550552 key = get_api_key()
640642 if datadir:
641643 cur_time = timestr()
642644 if cur_time != last_time:
643 last_time = cur_time
644 fout.close()
645 fout = open_streaming_file(datadir, last_time)
645 last_time = cur_time
646 fout.close()
647 fout = open_streaming_file(datadir, last_time)
646648 helpers.write_banner(fout, banner)
647649
648650 # Print the banner information to stdout
705707 click.echo(click.style('Not a honeypot', fg='green'))
706708
707709 click.echo('Score: {}'.format(score))
708 except:
710 except Exception:
709711 raise click.ClickException('Unable to calculate honeyscore')
710712
711713
724726 except Exception as e:
725727 raise click.ClickException(u'{}'.format(e))
726728
729
727730 if __name__ == '__main__':
728731 main()
+0
-9
shodan/alert.py less more
0 class Alert:
1 def __init__(self):
2 self.id = None
3 self.name = None
4 self.api_key = None
5 self.filters = None
6 self.credits = None
7 self.created = None
8 self.expires = None
00 import click
11 import shodan
22
3 from operator import itemgetter
34 from shodan.cli.helpers import get_api_key
5
46
57 @click.group()
68 def alert():
2325 except shodan.APIError as e:
2426 raise click.ClickException(e.value)
2527 click.echo("Alerts deleted")
28
2629
2730 @alert.command(name='create')
2831 @click.argument('name', metavar='<name>')
4144 click.secho('Successfully created network alert!', fg='green')
4245 click.secho('Alert ID: {}'.format(alert['id']), fg='cyan')
4346
47
48 @alert.command(name='info')
49 @click.argument('alert', metavar='<alert id>')
50 def alert_info(alert):
51 """Show information about a specific alert"""
52 key = get_api_key()
53 api = shodan.Shodan(key)
54
55 try:
56 info = api.alerts(aid=alert)
57 except shodan.APIError as e:
58 raise click.ClickException(e.value)
59
60 click.secho(info['name'], fg='cyan')
61 click.secho('Created: ', nl=False, dim=True)
62 click.secho(info['created'], fg='magenta')
63
64 click.secho('Notifications: ', nl=False, dim=True)
65 if 'triggers' in info and info['triggers']:
66 click.secho('enabled', fg='green')
67 else:
68 click.echo('disabled')
69
70 click.echo('')
71 click.secho('Network Range(s):', dim=True)
72
73 for network in info['filters']['ip']:
74 click.echo(u' > {}'.format(click.style(network, fg='yellow')))
75
76 click.echo('')
77 if 'triggers' in info and info['triggers']:
78 click.secho('Triggers:', dim=True)
79 for trigger in info['triggers']:
80 click.echo(u' > {}'.format(click.style(trigger, fg='yellow')))
81 click.echo('')
82
83
4484 @alert.command(name='list')
4585 @click.option('--expired', help='Whether or not to show expired alerts.', default=True, type=bool)
4686 def alert_list(expired):
5696
5797 if len(results) > 0:
5898 click.echo(u'# {:14} {:<21} {:<15s}'.format('Alert ID', 'Name', 'IP/ Network'))
59 # click.echo('#' * 65)
99
60100 for alert in results:
61101 click.echo(
62102 u'{:16} {:<30} {:<35} '.format(
63 click.style(alert['id'], fg='yellow'),
103 click.style(alert['id'], fg='yellow'),
64104 click.style(alert['name'], fg='cyan'),
65105 click.style(', '.join(alert['filters']['ip']), fg='white')
66106 ),
67107 nl=False
68108 )
109
110 if 'triggers' in alert and alert['triggers']:
111 click.secho('Triggers: ', fg='magenta', nl=False)
112 click.echo(', '.join(alert['triggers'].keys()), nl=False)
69113
70114 if 'expired' in alert and alert['expired']:
71115 click.secho('expired', fg='red')
88132 except shodan.APIError as e:
89133 raise click.ClickException(e.value)
90134 click.echo("Alert deleted")
135
136
137 @alert.command(name='triggers')
138 def alert_list_triggers():
139 """List the available notification triggers"""
140 key = get_api_key()
141
142 # Get the list
143 api = shodan.Shodan(key)
144 try:
145 results = api.alert_triggers()
146 except shodan.APIError as e:
147 raise click.ClickException(e.value)
148
149 if len(results) > 0:
150 click.secho('The following triggers can be enabled on alerts:', dim=True)
151 click.echo('')
152
153 for trigger in sorted(results, key=itemgetter('name')):
154 click.secho('{:<12} '.format('Name'), dim=True, nl=False)
155 click.secho(trigger['name'], fg='yellow')
156
157 click.secho('{:<12} '.format('Description'), dim=True, nl=False)
158 click.secho(trigger['description'], fg='cyan')
159
160 click.secho('{:<12} '.format('Rule'), dim=True, nl=False)
161 click.echo(trigger['rule'])
162
163 click.echo('')
164 else:
165 click.echo("No triggers currently available.")
166
167
168 @alert.command(name='enable')
169 @click.argument('alert_id', metavar='<alert ID>')
170 @click.argument('trigger', metavar='<trigger name>')
171 def alert_enable_trigger(alert_id, trigger):
172 """Enable a trigger for the alert"""
173 key = get_api_key()
174
175 # Get the list
176 api = shodan.Shodan(key)
177 try:
178 api.enable_alert_trigger(alert_id, trigger)
179 except shodan.APIError as e:
180 raise click.ClickException(e.value)
181
182 click.secho('Successfully enabled the trigger: {}'.format(trigger), fg='green')
183
184
185 @alert.command(name='disable')
186 @click.argument('alert_id', metavar='<alert ID>')
187 @click.argument('trigger', metavar='<trigger name>')
188 def alert_disable_trigger(alert_id, trigger):
189 """Disable a trigger for the alert"""
190 key = get_api_key()
191
192 # Get the list
193 api = shodan.Shodan(key)
194 try:
195 api.disable_alert_trigger(alert_id, trigger)
196 except shodan.APIError as e:
197 raise click.ClickException(e.value)
198
199 click.secho('Successfully disabled the trigger: {}'.format(trigger), fg='green')
11 from .excel import ExcelConverter
22 from .geojson import GeoJsonConverter
33 from .images import ImagesConverter
4 from .kml import KmlConverter
4 from .kml import KmlConverter
22
33 def __init__(self, fout):
44 self.fout = fout
5
5
66 def process(self, fout):
77 pass
2323 'os',
2424 'asn',
2525 'port',
26 'tags',
27 'timestamp',
2628 'transport',
2729 'product',
2830 'version',
29
31 'vulns',
32
3033 'ssl.cipher.version',
3134 'ssl.cipher.bits',
3235 'ssl.cipher.name',
3538 'ssl.cert.serial',
3639 'ssl.cert.fingerprint.sha1',
3740 'ssl.cert.fingerprint.sha256',
38
41
3942 'html',
4043 'title',
4144 ]
42
45
4346 def process(self, files):
4447 writer = csv_writer(self.fout, dialect=excel)
45
48
4649 # Write the header
4750 writer.writerow(self.fields)
48
51
4952 for banner in iterate_files(files):
53 # The "vulns" property can't be nicely flattened as-is so we turn
54 # it into a list before processing the banner.
55 if 'vulns' in banner:
56 banner['vulns'] = banner['vulns'].keys()
57
5058 try:
5159 row = []
5260 for field in self.fields:
5563 writer.writerow(row)
5664 except Exception:
5765 pass
58
66
5967 def banner_field(self, banner, flat_field):
6068 # The provided field is a collapsed form of the actual field
6169 fields = flat_field.split('.')
62
70
6371 try:
6472 current_obj = banner
6573 for field in fields:
6674 current_obj = current_obj[field]
67
75
6876 # Convert a list into a concatenated string
6977 if isinstance(current_obj, list):
7078 current_obj = ','.join([str(i) for i in current_obj])
71
79
7280 return current_obj
7381 except Exception:
7482 pass
75
83
7684 return ''
77
85
7886 def flatten(self, d, parent_key='', sep='.'):
7987 items = []
8088 for k, v in d.items():
8189 new_key = parent_key + sep + k if parent_key else k
8290 if isinstance(v, MutableMapping):
83 # pylint: disable=E0602
84 items.extend(flatten(v, new_key, sep=sep).items())
91 items.extend(self.flatten(v, new_key, sep=sep).items())
8592 else:
8693 items.append((new_key, v))
8794 return dict(items)
2222 'transport',
2323 'product',
2424 'version',
25
25
2626 'http.server',
2727 'http.title',
2828 ]
3939 'http.server': 'Web Server',
4040 'http.title': 'Website Title',
4141 }
42
42
4343 def process(self, files):
4444 # Get the filename from the already-open file handle
4545 filename = self.fout.name
5454 bold = workbook.add_format({
5555 'bold': 1,
5656 })
57
57
5858 # Create the main worksheet where all the raw data is shown
5959 main_sheet = workbook.add_worksheet('Raw Data')
6060
6161 # Write the header
62 main_sheet.write(0, 0, 'IP', bold) # The IP field can be either ip_str or ipv6 so we treat it differently
62 main_sheet.write(0, 0, 'IP', bold) # The IP field can be either ip_str or ipv6 so we treat it differently
6363 main_sheet.set_column(0, 0, 20)
64
64
6565 row = 0
6666 col = 1
6767 for field in self.fields:
7979 for field in self.fields:
8080 value = self.banner_field(banner, field)
8181 data.append(value)
82
82
8383 # Write those values to the main workbook
8484 # Starting off w/ the special "IP" property
8585 main_sheet.write_string(row, 0, get_ip(banner))
9191 row += 1
9292 except Exception:
9393 pass
94
94
9595 # Aggregate summary information
9696 total += 1
9797 ports[banner['port']] += 1
98
98
9999 summary_sheet = workbook.add_worksheet('Summary')
100100 summary_sheet.write(0, 0, 'Total', bold)
101101 summary_sheet.write(0, 1, total)
108108 summary_sheet.write(row, col, key)
109109 summary_sheet.write(row, col + 1, value)
110110 row += 1
111
111
112112 def banner_field(self, banner, flat_field):
113113 # The provided field is a collapsed form of the actual field
114114 fields = flat_field.split('.')
115
115
116116 try:
117117 current_obj = banner
118118 for field in fields:
119119 current_obj = current_obj[field]
120
120
121121 # Convert a list into a concatenated string
122122 if isinstance(current_obj, list):
123123 current_obj = ','.join([str(i) for i in current_obj])
124
124
125125 return current_obj
126126 except Exception:
127127 pass
128
128
129129 return ''
11 from .base import Converter
22 from ...helpers import get_ip, iterate_files
33
4
45 class GeoJsonConverter(Converter):
5
6
67 def header(self):
78 self.fout.write("""{
89 "type": "FeatureCollection",
910 "features": [
1011 """)
11
12
1213 def footer(self):
1314 self.fout.write("""{ }]}""")
14
15
1516 def process(self, files):
1617 # Write the header
1718 self.header()
18
19
1920 hosts = {}
2021 for banner in iterate_files(files):
2122 ip = get_ip(banner)
2223 if not ip:
2324 continue
24
25
2526 if ip not in hosts:
2627 hosts[ip] = banner
2728 hosts[ip]['ports'] = []
28
29
2930 hosts[ip]['ports'].append(banner['port'])
30
31
3132 for ip, host in iter(hosts.items()):
3233 self.write(host)
33
34
3435 self.footer()
35
36
36
3737 def write(self, host):
3838 try:
3939 ip = get_ip(host)
1313 # special code in the Shodan CLI that relies on the "dirname" property to let
1414 # the user know where the images have been stored.
1515 dirname = None
16
16
1717 def process(self, files):
1818 # Get the filename from the already-open file handle and use it as
1919 # the directory name to store the images.
11 from .base import Converter
22 from ...helpers import iterate_files
33
4
45 class KmlConverter(Converter):
5
6
67 def header(self):
78 self.fout.write("""<?xml version="1.0" encoding="UTF-8"?>
89 <kml xmlns="http://www.opengis.net/kml/2.2">
910 <Document>""")
10
11
1112 def footer(self):
1213 self.fout.write("""</Document></kml>""")
13
14
1415 def process(self, files):
1516 # Write the header
1617 self.header()
17
18
1819 hosts = {}
1920 for banner in iterate_files(files):
2021 ip = banner.get('ip_str', banner.get('ipv6', None))
2122 if not ip:
2223 continue
23
24
2425 if ip not in hosts:
2526 hosts[ip] = banner
2627 hosts[ip]['ports'] = []
27
28
2829 hosts[ip]['ports'].append(banner['port'])
29
30
3031 for ip, host in iter(hosts.items()):
3132 self.write(host)
32
33
3334 self.footer()
34
35
35
3636 def write(self, host):
3737 try:
3838 ip = host.get('ip_str', host.get('ipv6', None))
88 import sys
99
1010 from .settings import SHODAN_CONFIG_DIR
11
12 try:
13 basestring # Python 2
14 except NameError:
15 basestring = (str, ) # Python 3
16
1117
1218 def get_api_key():
1319 '''Returns the API key of the current logged-in user.'''
6363 for port in ports:
6464 banner = {
6565 'port': port,
66 'transport': 'tcp', # All the filtered services use TCP
67 'timestamp': host['data'][-1]['timestamp'], # Use the timestamp of the oldest banner
68 'placeholder': True, # Don't store this banner when the file is saved
66 'transport': 'tcp', # All the filtered services use TCP
67 'timestamp': host['data'][-1]['timestamp'], # Use the timestamp of the oldest banner
68 'placeholder': True, # Don't store this banner when the file is saved
6969 }
7070 host['data'].append(banner)
7171
9393 # Show optional ssl info
9494 if 'ssl' in banner:
9595 if 'versions' in banner['ssl'] and banner['ssl']['versions']:
96 click.echo('\t|-- SSL Versions: {}'.format(', '.join([version for version in sorted(banner['ssl']['versions']) if not version.startswith('-')])))
96 click.echo('\t|-- SSL Versions: {}'.format(', '.join([item for item in sorted(banner['ssl']['versions']) if not version.startswith('-')])))
9797 if 'dhparams' in banner['ssl'] and banner['ssl']['dhparams']:
9898 click.echo('\t|-- Diffie-Hellman Parameters:')
9999 click.echo('\t\t{:15s}{}\n\t\t{:15s}{}'.format('Bits:', banner['ssl']['dhparams']['bits'], 'Generator:', banner['ssl']['dhparams']['generator']))
118118 HOST_PRINT = {
119119 'pretty': host_print_pretty,
120120 'tsv': host_print_tsv,
121 }
121 }
2121 api.org.add_member(user, notify=not silent)
2222 except shodan.APIError as e:
2323 raise click.ClickException(e.value)
24
24
2525 click.secho('Successfully added the new member', fg='green')
2626
2727
3838 click.secho(organization['name'], fg='cyan')
3939 click.secho('Access Level: ', nl=False, dim=True)
4040 click.secho(humanize_api_plan(organization['upgrade_type']), fg='magenta')
41
41
4242 if organization['domains']:
4343 click.secho('Authorized Domains: ', nl=False, dim=True)
4444 click.echo(', '.join(organization['domains']))
45
45
4646 click.echo('')
4747 click.secho('Administrators:', dim=True)
4848
5050 click.echo(u' > {:30}\t{:30}'.format(
5151 click.style(admin['username'], fg='yellow'),
5252 admin['email'])
53 )
54
53 )
54
5555 click.echo('')
5656 if organization['members']:
5757 click.secho('Members:', dim=True)
7575 api.org.remove_member(user)
7676 except shodan.APIError as e:
7777 raise click.ClickException(e.value)
78
78
7979 click.secho('Successfully removed the member', fg='green')
1414 def scan():
1515 """Scan an IP/ netblock using Shodan."""
1616 pass
17
18
19 @scan.command(name='list')
20 def scan_list():
21 """Show recently launched scans"""
22 key = get_api_key()
23
24 # Get the list
25 api = shodan.Shodan(key)
26 try:
27 scans = api.scans()
28 except shodan.APIError as e:
29 raise click.ClickException(e.value)
30
31 if len(scans) > 0:
32 click.echo(u'# {} Scans Total - Showing 10 most recent scans:'.format(scans['total']))
33 click.echo(u'# {:20} {:<15} {:<10} {:<15s}'.format('Scan ID', 'Status', 'Size', 'Timestamp'))
34 # click.echo('#' * 65)
35 for scan in scans['matches'][:10]:
36 click.echo(
37 u'{:31} {:<24} {:<10} {:<15s}'.format(
38 click.style(scan['id'], fg='yellow'),
39 click.style(scan['status'], fg='cyan'),
40 scan['size'],
41 scan['created']
42 )
43 )
44 else:
45 click.echo("You haven't yet launched any scans.")
1746
1847
1948 @scan.command(name='internet')
5786
5887 if not quiet:
5988 click.echo('{0:<40} {1:<20} {2}'.format(
60 click.style(helpers.get_ip(banner), fg=COLORIZE_FIELDS['ip_str']),
61 click.style(str(banner['port']), fg=COLORIZE_FIELDS['port']),
62 ';'.join(banner['hostnames'])
63 )
89 click.style(helpers.get_ip(banner), fg=COLORIZE_FIELDS['ip_str']),
90 click.style(str(banner['port']), fg=COLORIZE_FIELDS['port']),
91 ';'.join(banner['hostnames']))
6492 )
6593 except shodan.APIError as e:
6694 # We stop waiting for results if the scan has been processed by the crawlers and
107107 TODO: filter out stuff that doesn't fit
108108 TODO: make it possible to use "zoomed" maps
109109 """
110 width = (self.corners[3]-self.corners[1])
111 height = (self.corners[2]-self.corners[0])
110 width = (self.corners[3] - self.corners[1])
111 height = (self.corners[2] - self.corners[0])
112112
113113 # change to 0-180, 0-360
114 abs_lat = -lat+90
115 abs_lon = lon+180
116 x = (abs_lon/360.0)*width + self.corners[1]
117 y = (abs_lat/180.0)*height + self.corners[0]
114 abs_lat = -lat + 90
115 abs_lon = lon + 180
116 x = (abs_lon / 360.0) * width + self.corners[1]
117 y = (abs_lat / 180.0) * height + self.corners[0]
118118 return int(x), int(y)
119119
120120 def set_data(self, data):
154154 self.window.addstr(0, 0, self.map)
155155
156156 # FIXME: position to be defined in map config?
157 row = self.corners[2]-6
157 row = self.corners[2] - 6
158158 items_to_show = 5
159159 for lat, lon, char, desc, attrs, color in self.data:
160160 # to make this work almost everywhere. see http://docs.python.org/2/library/curses.html
176176 self.window.addstr(row, 1, det_show, attrs)
177177 row += 1
178178 items_to_show -= 1
179 except StandardError:
179 except Exception:
180180 # FIXME: check window size before addstr()
181181 break
182182 self.window.overwrite(target)
256256 api = Shodan(get_api_key())
257257 return launch_map(api)
258258
259
259260 if __name__ == '__main__':
260261 import sys
261262 sys.exit(main())
175175 :param key: The Shodan API key.
176176 :type key: str
177177 :param proxies: A proxies array for the requests library, e.g. {'https': 'your proxy'}
178 :type key: dict
178 :type proxies: dict
179179 """
180180 self.api_key = key
181181 self.base_url = 'https://api.shodan.io'
346346
347347 return self._request('/shodan/scan', params, method='post')
348348
349 def scans(self, page=1):
350 """Get a list of scans submitted
351
352 :param page: Page through the list of scans 100 results at a time
353 :type page: int
354 """
355 return self._request('/shodan/scans', {
356 'page': page,
357 })
358
349359 def scan_internet(self, port, protocol):
350360 """Scan a network using Shodan
351361
437447 try:
438448 yield banner
439449 except GeneratorExit:
440 return # exit out of the function
450 return # exit out of the function
441451 page += 1
442452 tries = 0
443453 except Exception:
446456 break
447457
448458 tries += 1
449 time.sleep(1.0) # wait 1 second if the search errored out for some reason
459 time.sleep(1.0) # wait 1 second if the search errored out for some reason
450460
451461 def search_tokens(self, query):
452462 """Returns information about the search query itself (filters used etc.)
506516 def queries_tags(self, size=10):
507517 """Search the directory of saved search queries in Shodan.
508518
509 :param query: The number of tags to return
510 :type page: int
519 :param size: The number of tags to return
520 :type size: int
511521
512522 :returns: A list of tags.
513523 """
517527 return self._request('/shodan/query/tags', args)
518528
519529 def create_alert(self, name, ip, expires=0):
520 """Search the directory of saved search queries in Shodan.
521
522 :param query: The number of tags to return
523 :type page: int
524
525 :returns: A list of tags.
530 """Create a network alert/ private firehose for the specified IP range(s)
531
532 :param name: Name of the alert
533 :type name: str
534 :param ip: Network range(s) to monitor
535 :type ip: str OR list of str
536
537 :returns: A dict describing the alert
526538 """
527539 data = {
528540 'name': name,
546558
547559 response = api_request(self.api_key, func, params={
548560 'include_expired': include_expired,
549 },
550 proxies=self._session.proxies)
561 }, proxies=self._session.proxies)
551562
552563 return response
553564
560571
561572 return response
562573
574 def alert_triggers(self):
575 """Return a list of available triggers that can be enabled for alerts.
576
577 :returns: A list of triggers
578 """
579 return self._request('/shodan/alert/triggers', {})
580
581 def enable_alert_trigger(self, aid, trigger):
582 """Enable the given trigger on the alert."""
583 return self._request('/shodan/alert/{}/trigger/{}'.format(aid, trigger), {}, method='put')
584
585 def disable_alert_trigger(self, aid, trigger):
586 """Disable the given trigger on the alert."""
587 return self._request('/shodan/alert/{}/trigger/{}'.format(aid, trigger), {}, method='delete')
11 """This exception gets raised whenever a non-200 status code was returned by the Shodan API."""
22 def __init__(self, value):
33 self.value = value
4
4
55 def __str__(self):
66 return self.value
77
88
99 class APITimeout(APIError):
10 pass
11
10 pass
1818 if isinstance(facet, basestring):
1919 facet_str += facet
2020 else:
21 facet_str += '%s:%s' % (facet[0], facet[1])
21 facet_str += '{}:{}'.format(facet[0], facet[1])
2222 facet_str += ','
2323 return facet_str[:-1]
2424
7575 # Parse the text into JSON
7676 try:
7777 data = data.json()
78 except:
78 except Exception:
7979 raise APIError('Unable to parse JSON response')
8080
8181 # Raise an exception if an error occurred
112112
113113 for line in fin:
114114 # Ensure the line has been decoded into a string to prevent errors w/ Python3
115 line = line.decode('utf-8')
115 if not isinstance(line, basestring):
116 line = line.decode('utf-8')
116117
117118 # Convert the JSON into a native Python object
118119 banner = loads(line)
119120 yield banner
121
120122
121123 def get_screenshot(banner):
122124 if 'opts' in banner and 'screenshot' in banner['opts']:
158160 >>> humanize_bytes(1024*1234*1111,1)
159161 '1.3 GB'
160162 """
161
162163 if bytes == 1:
163164 return '1 byte'
164165 if bytes < 1024:
165166 return '%.*f %s' % (precision, bytes, "bytes")
166167
167168 suffixes = ['KB', 'MB', 'GB', 'TB', 'PB']
168 multiple = 1024.0 #.0 force float on python 2
169 multiple = 1024.0 # .0 to force float on python 2
169170 for suffix in suffixes:
170171 bytes /= multiple
171172 if bytes < multiple:
2020
2121 # The user doesn't want to use a timeout
2222 # If the timeout is specified as 0 then we also don't want to have a timeout
23 if ( timeout and timeout <= 0 ) or ( timeout == 0 ):
23 if (timeout and timeout <= 0) or (timeout == 0):
2424 timeout = None
2525
2626 # If the user requested a timeout then we need to disable heartbeat messages
4242 # not specific to Cloudflare.
4343 if req.status_code != 524 or timeout >= 0:
4444 break
45 except Exception as e:
45 except Exception:
4646 raise APIError('Unable to contact the Shodan Streaming API')
4747
4848 if req.status_code != 200:
4949 try:
5050 data = json.loads(req.text)
5151 raise APIError(data['error'])
52 except APIError as e:
52 except APIError:
5353 raise
54 except Exception as e:
54 except Exception:
5555 pass
5656 raise APIError('Invalid API key or you do not have access to the Streaming API')
5757 if req.encoding is None:
7777 try:
7878 for line in self._iter_stream(stream, raw):
7979 yield line
80 except requests.exceptions.ConnectionError as e:
80 except requests.exceptions.ConnectionError:
8181 raise APIError('Stream timed out')
82 except ssl.SSLError as e:
82 except ssl.SSLError:
8383 raise APIError('Stream timed out')
8484
8585 def asn(self, asn, raw=False, timeout=None):
122122 stream = self._create_stream('/shodan/ports/%s' % ','.join([str(port) for port in ports]), timeout=timeout)
123123 for line in self._iter_stream(stream, raw):
124124 yield line
125
2323 try:
2424 req = requests.get(self.base_url + name, params={'key': self.parent.api_key},
2525 stream=True, proxies=self.proxies)
26 except:
26 except Exception:
2727 raise APIError('Unable to contact the Shodan Streaming API')
2828
2929 if req.status_code != 200:
3030 try:
3131 raise APIError(req.json()['error'])
32 except:
32 except Exception:
3333 pass
3434 raise APIError('Invalid API key or you do not have access to the Streaming API')
3535 return req
6464 self.api_key = key
6565 self.base_url = 'https://api.shodan.io'
6666 self.stream = self.Stream(self)
67
88
99 class ShodanTests(unittest.TestCase):
1010
11 api = None
12 FACETS = [
13 'port',
14 ('domain', 1)
15 ]
16 QUERIES = {
17 'simple': 'cisco-ios',
18 'minify': 'apache',
19 'advanced': 'apache port:443',
20 'empty': 'asdasdasdasdasdasdasdasdasdhjihjkjk',
21 }
11 api = None
12 FACETS = [
13 'port',
14 ('domain', 1)
15 ]
16 QUERIES = {
17 'simple': 'cisco-ios',
18 'minify': 'apache',
19 'advanced': 'apache port:443',
20 'empty': 'asdasdasdasdasdasdasdasdasdhjihjkjk',
21 }
2222
23 def setUp(self):
24 self.api = shodan.Shodan(open('SHODAN-API-KEY').read().strip())
23 def setUp(self):
24 self.api = shodan.Shodan(open('SHODAN-API-KEY').read().strip())
2525
26 def test_search_simple(self):
27 results = self.api.search(self.QUERIES['simple'])
26 def test_search_simple(self):
27 results = self.api.search(self.QUERIES['simple'])
2828
29 # Make sure the properties exist
30 self.assertIn('matches', results)
31 self.assertIn('total', results)
29 # Make sure the properties exist
30 self.assertIn('matches', results)
31 self.assertIn('total', results)
3232
33 # Make sure no error occurred
34 self.assertNotIn('error', results)
33 # Make sure no error occurred
34 self.assertNotIn('error', results)
3535
36 # Make sure some values were returned
37 self.assertTrue(results['matches'])
38 self.assertTrue(results['total'])
36 # Make sure some values were returned
37 self.assertTrue(results['matches'])
38 self.assertTrue(results['total'])
3939
40 # A regular search shouldn't have the optional info
41 self.assertNotIn('opts', results['matches'][0])
40 # A regular search shouldn't have the optional info
41 self.assertNotIn('opts', results['matches'][0])
4242
43 def test_search_empty(self):
44 results = self.api.search(self.QUERIES['empty'])
45 self.assertTrue(len(results['matches']) == 0)
46 self.assertEqual(results['total'], 0)
43 def test_search_empty(self):
44 results = self.api.search(self.QUERIES['empty'])
45 self.assertTrue(len(results['matches']) == 0)
46 self.assertEqual(results['total'], 0)
4747
48 def test_search_facets(self):
49 results = self.api.search(self.QUERIES['simple'], facets=self.FACETS)
48 def test_search_facets(self):
49 results = self.api.search(self.QUERIES['simple'], facets=self.FACETS)
5050
51 self.assertTrue(results['facets']['port'])
52 self.assertEqual(len(results['facets']['domain']), 1)
51 self.assertTrue(results['facets']['port'])
52 self.assertEqual(len(results['facets']['domain']), 1)
5353
54 def test_count_simple(self):
55 results = self.api.count(self.QUERIES['simple'])
54 def test_count_simple(self):
55 results = self.api.count(self.QUERIES['simple'])
5656
57 # Make sure the properties exist
58 self.assertIn('matches', results)
59 self.assertIn('total', results)
57 # Make sure the properties exist
58 self.assertIn('matches', results)
59 self.assertIn('total', results)
6060
61 # Make sure no error occurred
62 self.assertNotIn('error', results)
61 # Make sure no error occurred
62 self.assertNotIn('error', results)
6363
64 # Make sure no values were returned
65 self.assertFalse(results['matches'])
66 self.assertTrue(results['total'])
64 # Make sure no values were returned
65 self.assertFalse(results['matches'])
66 self.assertTrue(results['total'])
6767
68 def test_count_facets(self):
69 results = self.api.count(self.QUERIES['simple'], facets=self.FACETS)
68 def test_count_facets(self):
69 results = self.api.count(self.QUERIES['simple'], facets=self.FACETS)
7070
71 self.assertTrue(results['facets']['port'])
72 self.assertEqual(len(results['facets']['domain']), 1)
71 self.assertTrue(results['facets']['port'])
72 self.assertEqual(len(results['facets']['domain']), 1)
7373
74 def test_host_details(self):
75 host = self.api.host('147.228.101.7')
74 def test_host_details(self):
75 host = self.api.host('147.228.101.7')
7676
77 self.assertEqual('147.228.101.7', host['ip_str'])
78 self.assertFalse(isinstance(host['ip'], basestring))
77 self.assertEqual('147.228.101.7', host['ip_str'])
78 self.assertFalse(isinstance(host['ip'], basestring))
7979
80 def test_search_minify(self):
81 results = self.api.search(self.QUERIES['minify'], minify=False)
82 self.assertIn('opts', results['matches'][0])
80 def test_search_minify(self):
81 results = self.api.search(self.QUERIES['minify'], minify=False)
82 self.assertIn('opts', results['matches'][0])
8383
84 def test_exploits_search(self):
85 results = self.api.exploits.search('apache')
86 self.assertIn('matches', results)
87 self.assertIn('total', results)
88 self.assertTrue(results['matches'])
84 def test_exploits_search(self):
85 results = self.api.exploits.search('apache')
86 self.assertIn('matches', results)
87 self.assertIn('total', results)
88 self.assertTrue(results['matches'])
8989
90 def test_exploits_search_paging(self):
91 results = self.api.exploits.search('apache', page=1)
92 match1 = results['matches'][0]
93 results = self.api.exploits.search('apache', page=2)
94 match2 = results['matches'][0]
90 def test_exploits_search_paging(self):
91 results = self.api.exploits.search('apache', page=1)
92 match1 = results['matches'][0]
93 results = self.api.exploits.search('apache', page=2)
94 match2 = results['matches'][0]
9595
96 self.assertNotEqual(match1['_id'], match2['_id'])
96 self.assertNotEqual(match1['_id'], match2['_id'])
9797
98 def test_exploits_search_facets(self):
99 results = self.api.exploits.search('apache', facets=['source', ('author', 1)])
100 self.assertIn('facets', results)
101 self.assertTrue(results['facets']['source'])
102 self.assertTrue(len(results['facets']['author']) == 1)
98 def test_exploits_search_facets(self):
99 results = self.api.exploits.search('apache', facets=['source', ('author', 1)])
100 self.assertIn('facets', results)
101 self.assertTrue(results['facets']['source'])
102 self.assertTrue(len(results['facets']['author']) == 1)
103103
104 def test_exploits_count(self):
105 results = self.api.exploits.count('apache')
106 self.assertIn('matches', results)
107 self.assertIn('total', results)
108 self.assertTrue(len(results['matches']) == 0)
104 def test_exploits_count(self):
105 results = self.api.exploits.count('apache')
106 self.assertIn('matches', results)
107 self.assertIn('total', results)
108 self.assertTrue(len(results['matches']) == 0)
109109
110 def test_exploits_count_facets(self):
111 results = self.api.exploits.count('apache', facets=['source', ('author', 1)])
112 self.assertEqual(len(results['matches']), 0)
113 self.assertIn('facets', results)
114 self.assertTrue(results['facets']['source'])
115 self.assertTrue(len(results['facets']['author']) == 1)
110 def test_exploits_count_facets(self):
111 results = self.api.exploits.count('apache', facets=['source', ('author', 1)])
112 self.assertEqual(len(results['matches']), 0)
113 self.assertIn('facets', results)
114 self.assertTrue(results['facets']['source'])
115 self.assertTrue(len(results['facets']['author']) == 1)
116116
117 # Test error responses
118 def test_invalid_key(self):
119 api = shodan.Shodan('garbage')
120 raised = False
121 try:
122 api.search('something')
123 except shodan.APIError as e:
124 raised = True
117 # Test error responses
118 def test_invalid_key(self):
119 api = shodan.Shodan('garbage')
120 raised = False
121 try:
122 api.search('something')
123 except shodan.APIError:
124 raised = True
125125
126 self.assertTrue(raised)
126 self.assertTrue(raised)
127127
128 def test_invalid_host_ip(self):
129 raised = False
130 try:
131 host = self.api.host('test')
132 except shodan.APIError as e:
133 raised = True
128 def test_invalid_host_ip(self):
129 raised = False
130 try:
131 self.api.host('test')
132 except shodan.APIError:
133 raised = True
134134
135 self.assertTrue(raised)
135 self.assertTrue(raised)
136136
137 def test_search_empty_query(self):
138 raised = False
139 try:
140 self.api.search('')
141 except shodan.APIError as e:
142 raised = True
143 self.assertTrue(raised)
137 def test_search_empty_query(self):
138 raised = False
139 try:
140 self.api.search('')
141 except shodan.APIError:
142 raised = True
143 self.assertTrue(raised)
144144
145 def test_search_advanced_query(self):
146 # The free API plan can't use filters
147 raised = False
148 try:
149 self.api.search(self.QUERIES['advanced'])
150 except shodan.APIError as e:
151 raised = True
152 self.assertTrue(raised)
145 def test_search_advanced_query(self):
146 # The free API plan can't use filters
147 raised = False
148 try:
149 self.api.search(self.QUERIES['advanced'])
150 except shodan.APIError:
151 raised = True
152 self.assertTrue(raised)
153153
154154
155155 if __name__ == '__main__':
0 [flake8]
1 ignore =
2 E501
3
4 exclude =
5 build,
6 docs,
7 shodan.egg-info,
8 tmp,
9
10 per-file-ignores =
11 shodan/__init__.py:F401,
12 shodan/cli/converter/__init__.py:F401,
13 shodan/cli/worldmap.py:W291,W293,W605,