Skip to content

Commit

Permalink
Add tsv output format for shodan host (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
John Matherly committed Aug 17, 2018
1 parent 743b445 commit a3e5855
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 93 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ unreleased
----------
* The CHANGELOG is now part of the packages.
* Improved unicode handling in Python2 (#78)
* Add `tsv` output format for **shodan host** (#65)

1.9.0
-----
Expand Down
97 changes: 4 additions & 93 deletions shodan/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@

# Helper methods
from shodan.cli.helpers import get_api_key
from shodan.cli.host import HOST_PRINT

# Allow 3rd-parties to develop custom commands
from click_plugins import with_plugins
Expand Down Expand Up @@ -457,7 +458,7 @@ def download(limit, filename, query):


@main.command()
@click.option('--format', help='The output format for the host information. Possible values are: pretty, csv, tsv. (placeholder)', default='pretty', type=str)
@click.option('--format', help='The output format for the host information. Possible values are: pretty, tsv.', default='pretty', type=click.Choice(['pretty', 'tsv']))
@click.option('--history', help='Show the complete history of the host.', default=False, is_flag=True)
@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)
Expand All @@ -470,98 +471,8 @@ def host(format, history, filename, save, ip):
try:
host = api.host(ip, history=history)

# General info
click.echo(click.style(ip, fg='green'))
if len(host['hostnames']) > 0:
click.echo(u'{:25s}{}'.format('Hostnames:', ';'.join(host['hostnames'])))

if 'city' in host and host['city']:
click.echo(u'{:25s}{}'.format('City:', host['city']))

if 'country_name' in host and host['country_name']:
click.echo(u'{:25s}{}'.format('Country:', host['country_name']))

if 'os' in host and host['os']:
click.echo(u'{:25s}{}'.format('Operating System:', host['os']))

if 'org' in host and host['org']:
click.echo(u'{:25s}{}'.format('Organization:', host['org']))

if 'last_update' in host and host['last_update']:
click.echo('{:25s}{}'.format('Updated:', host['last_update']))

click.echo('{:25s}{}'.format('Number of open ports:', len(host['ports'])))

# Output the vulnerabilities the host has
if 'vulns' in host and len(host['vulns']) > 0:
vulns = []
for vuln in host['vulns']:
if vuln.startswith('!'):
continue
if vuln.upper() == 'CVE-2014-0160':
vulns.append(click.style('Heartbleed', fg='red'))
else:
vulns.append(click.style(vuln, fg='red'))

if len(vulns) > 0:
click.echo('{:25s}'.format('Vulnerabilities:'), nl=False)

for vuln in vulns:
click.echo(vuln + '\t', nl=False)

click.echo('')

click.echo('')

# If the user doesn't have access to SSL/ Telnet results then we need
# to pad the host['data'] property with empty banners so they still see
# the port listed as open. (#63)
if len(host['ports']) != len(host['data']):
# Find the ports the user can't see the data for
ports = host['ports']
for banner in host['data']:
if banner['port'] in ports:
ports.remove(banner['port'])

# Add the placeholder banners
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
}
host['data'].append(banner)

click.echo('Ports:')
for banner in sorted(host['data'], key=lambda k: k['port']):
product = ''
version = ''
if 'product' in banner and banner['product']:
product = banner['product']
if 'version' in banner and banner['version']:
version = '({})'.format(banner['version'])

click.echo(click.style('{:>7d}'.format(banner['port']), fg='cyan'), nl=False)
click.echo('/', nl=False)
click.echo(click.style('{} '.format(banner['transport']), fg='yellow'), nl=False)
click.echo('{} {}'.format(product, version), nl=False)

if history:
# Format the timestamp to only show the year-month-day
date = banner['timestamp'][:10]
click.echo(click.style('\t\t({})'.format(date), fg='white', dim=True), nl=False)
click.echo('')

# 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('-')])))
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']))
if 'fingerprint' in banner['ssl']['dhparams']:
click.echo('\t\t{:15s}{}'.format('Fingerprint:', banner['ssl']['dhparams']['fingerprint']))
# Print the host information to the terminal using the user-specified format
HOST_PRINT[format](host, history=history)

# Store the results
if filename or save:
Expand Down
121 changes: 121 additions & 0 deletions shodan/cli/host.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Helper methods for printing `host` information to the terminal.
import click

from shodan.helpers import get_ip


def host_print_pretty(host, history=False):
"""Show the host information in a user-friendly way and try to include
as much relevant information as possible."""
# General info
click.echo(click.style(get_ip(host), fg='green'))
if len(host['hostnames']) > 0:
click.echo(u'{:25s}{}'.format('Hostnames:', ';'.join(host['hostnames'])))

if 'city' in host and host['city']:
click.echo(u'{:25s}{}'.format('City:', host['city']))

if 'country_name' in host and host['country_name']:
click.echo(u'{:25s}{}'.format('Country:', host['country_name']))

if 'os' in host and host['os']:
click.echo(u'{:25s}{}'.format('Operating System:', host['os']))

if 'org' in host and host['org']:
click.echo(u'{:25s}{}'.format('Organization:', host['org']))

if 'last_update' in host and host['last_update']:
click.echo('{:25s}{}'.format('Updated:', host['last_update']))

click.echo('{:25s}{}'.format('Number of open ports:', len(host['ports'])))

# Output the vulnerabilities the host has
if 'vulns' in host and len(host['vulns']) > 0:
vulns = []
for vuln in host['vulns']:
if vuln.startswith('!'):
continue
if vuln.upper() == 'CVE-2014-0160':
vulns.append(click.style('Heartbleed', fg='red'))
else:
vulns.append(click.style(vuln, fg='red'))

if len(vulns) > 0:
click.echo('{:25s}'.format('Vulnerabilities:'), nl=False)

for vuln in vulns:
click.echo(vuln + '\t', nl=False)

click.echo('')

click.echo('')

# If the user doesn't have access to SSL/ Telnet results then we need
# to pad the host['data'] property with empty banners so they still see
# the port listed as open. (#63)
if len(host['ports']) != len(host['data']):
# Find the ports the user can't see the data for
ports = host['ports']
for banner in host['data']:
if banner['port'] in ports:
ports.remove(banner['port'])

# Add the placeholder banners
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
}
host['data'].append(banner)

click.echo('Ports:')
for banner in sorted(host['data'], key=lambda k: k['port']):
product = ''
version = ''
if 'product' in banner and banner['product']:
product = banner['product']
if 'version' in banner and banner['version']:
version = '({})'.format(banner['version'])

click.echo(click.style('{:>7d}'.format(banner['port']), fg='cyan'), nl=False)
click.echo('/', nl=False)
click.echo(click.style('{} '.format(banner['transport']), fg='yellow'), nl=False)
click.echo('{} {}'.format(product, version), nl=False)

if history:
# Format the timestamp to only show the year-month-day
date = banner['timestamp'][:10]
click.echo(click.style('\t\t({})'.format(date), fg='white', dim=True), nl=False)
click.echo('')

# 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('-')])))
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']))
if 'fingerprint' in banner['ssl']['dhparams']:
click.echo('\t\t{:15s}{}'.format('Fingerprint:', banner['ssl']['dhparams']['fingerprint']))


def host_print_tsv(host, history=False):
"""Show the host information in a succinct, grep-friendly manner."""
for banner in sorted(host['data'], key=lambda k: k['port']):
click.echo(click.style('{:>7d}'.format(banner['port']), fg='cyan'), nl=False)
click.echo('\t', nl=False)
click.echo(click.style('{} '.format(banner['transport']), fg='yellow'), nl=False)

if history:
# Format the timestamp to only show the year-month-day
date = banner['timestamp'][:10]
click.echo(click.style('\t({})'.format(date), fg='white', dim=True), nl=False)
click.echo('')


HOST_PRINT = {
'pretty': host_print_pretty,
'tsv': host_print_tsv,
}

0 comments on commit a3e5855

Please sign in to comment.