From f18c7626612a126b73924396c57a1d4dce27b9ca Mon Sep 17 00:00:00 2001 From: Richard Howe Date: Sat, 2 Dec 2023 23:19:34 -0500 Subject: [PATCH 1/4] Added input validation callback functions. --- shodan/__main__.py | 5 +++-- shodan/cli/validation.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 shodan/cli/validation.py diff --git a/shodan/__main__.py b/shodan/__main__.py index 4093b94..05c2494 100644 --- a/shodan/__main__.py +++ b/shodan/__main__.py @@ -37,6 +37,7 @@ import requests import time import json +from shodan.cli.validation import check_file_format, check_input_file_type # The file converters that are used to go from .json.gz to various other formats from shodan.cli.converter import CsvConverter, KmlConverter, GeoJsonConverter, ExcelConverter, ImagesConverter @@ -93,8 +94,8 @@ def main(): @main.command() @click.option('--fields', help='List of properties to output.', default=None) -@click.argument('input', metavar='', type=click.Path(exists=True)) -@click.argument('format', metavar='', type=click.Choice(CONVERTERS.keys())) +@click.argument('input', metavar='', type=click.Path(exists=True), callback=check_input_file_type) +@click.argument('format', metavar='', type=click.Choice(CONVERTERS.keys()), callback=check_file_format) def convert(fields, input, format): """Convert the given input data file into a different format. The following file formats are supported: diff --git a/shodan/cli/validation.py b/shodan/cli/validation.py new file mode 100644 index 0000000..1888392 --- /dev/null +++ b/shodan/cli/validation.py @@ -0,0 +1,29 @@ +import click + + +def check_input_file_type(ctx, param, value): + """ + Click callback method used for file type input validation. + :param ctx: Python Click library Context object. + :param param: Python Click Context object params attribute. + :param value: Value passed in for a given command line parameter. + """ + idx = value.find(".") + + if idx == -1 or value[idx:] != ".json.gz": + raise click.BadParameter("Input file type must be '.json.gz'") + return value + +def check_file_format(ctx, param, value): + """ + Click callback method used for output file format input validation. + :param ctx: Python Click library Context object. + :param param: Python Click Context object params attribute. + :param value: Value passed in for a given command line parameter. + """ + supported_file_types = ["kml", "csv", "geo.json", "images", "xlsx"] + file_type_str = ', '.join(supported_file_types) + + if value not in supported_file_types: + raise click.BadParameter(f"Output file type must be one of the supported file extensions:\n{file_type_str}") + return value From 4aadd31a8ccf0f5882e83595878418fd7ea745c7 Mon Sep 17 00:00:00 2001 From: Richard Howe Date: Sun, 3 Dec 2023 13:02:01 -0500 Subject: [PATCH 2/4] Updated implementation based on reviewer feedback. --- shodan/__main__.py | 2 +- shodan/cli/validation.py | 14 -------------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/shodan/__main__.py b/shodan/__main__.py index 05c2494..5b1f824 100644 --- a/shodan/__main__.py +++ b/shodan/__main__.py @@ -95,7 +95,7 @@ def main(): @main.command() @click.option('--fields', help='List of properties to output.', default=None) @click.argument('input', metavar='', type=click.Path(exists=True), callback=check_input_file_type) -@click.argument('format', metavar='', type=click.Choice(CONVERTERS.keys()), callback=check_file_format) +@click.argument('format', metavar='', type=click.Choice(CONVERTERS.keys())) def convert(fields, input, format): """Convert the given input data file into a different format. The following file formats are supported: diff --git a/shodan/cli/validation.py b/shodan/cli/validation.py index 1888392..da53b90 100644 --- a/shodan/cli/validation.py +++ b/shodan/cli/validation.py @@ -13,17 +13,3 @@ def check_input_file_type(ctx, param, value): if idx == -1 or value[idx:] != ".json.gz": raise click.BadParameter("Input file type must be '.json.gz'") return value - -def check_file_format(ctx, param, value): - """ - Click callback method used for output file format input validation. - :param ctx: Python Click library Context object. - :param param: Python Click Context object params attribute. - :param value: Value passed in for a given command line parameter. - """ - supported_file_types = ["kml", "csv", "geo.json", "images", "xlsx"] - file_type_str = ', '.join(supported_file_types) - - if value not in supported_file_types: - raise click.BadParameter(f"Output file type must be one of the supported file extensions:\n{file_type_str}") - return value From e443d2a912987fb5edae934853123f72f4fc9735 Mon Sep 17 00:00:00 2001 From: Richard Howe Date: Sun, 3 Dec 2023 15:31:08 -0500 Subject: [PATCH 3/4] Fixed import error. --- shodan/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shodan/__main__.py b/shodan/__main__.py index 5b1f824..1716e73 100644 --- a/shodan/__main__.py +++ b/shodan/__main__.py @@ -37,7 +37,7 @@ import requests import time import json -from shodan.cli.validation import check_file_format, check_input_file_type +from shodan.cli.validation import check_input_file_type # The file converters that are used to go from .json.gz to various other formats from shodan.cli.converter import CsvConverter, KmlConverter, GeoJsonConverter, ExcelConverter, ImagesConverter From 3ebeea9fb91b1d38b631c0450a5e875be709eb58 Mon Sep 17 00:00:00 2001 From: Richard Howe Date: Sun, 3 Dec 2023 16:12:37 -0500 Subject: [PATCH 4/4] Added input validation to check for empty strings and non-existent file paths. --- shodan/__main__.py | 28 ++++++---------------------- shodan/cli/validation.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/shodan/__main__.py b/shodan/__main__.py index 1716e73..d4ec5ea 100644 --- a/shodan/__main__.py +++ b/shodan/__main__.py @@ -37,7 +37,7 @@ import requests import time import json -from shodan.cli.validation import check_input_file_type +from shodan.cli.validation import check_input_file_type, check_filename_filepath, check_not_null # The file converters that are used to go from .json.gz to various other formats from shodan.cli.converter import CsvConverter, KmlConverter, GeoJsonConverter, ExcelConverter, ImagesConverter @@ -264,8 +264,8 @@ def count(query): @main.command() @click.option('--fields', help='Specify the list of properties to download instead of grabbing the full banner', default=None, type=str) @click.option('--limit', help='The number of results you want to download. -1 to download all the data possible.', default=1000, type=int) -@click.argument('filename', metavar='') -@click.argument('query', metavar='', nargs=-1) +@click.argument('filename', metavar='', callback=check_filename_filepath) +@click.argument('query', metavar='', nargs=-1, callback=check_not_null) def download(fields, limit, filename, query): """Download search results and save them in a compressed JSON file.""" key = get_api_key() @@ -273,14 +273,6 @@ def download(fields, limit, filename, query): # Create the query string out of the provided tuple query = ' '.join(query).strip() - # Make sure the user didn't supply an empty string - if query == '': - raise click.ClickException('Empty search query') - - filename = filename.strip() - if filename == '': - raise click.ClickException('Empty filename') - # Add the appropriate extension if it's not there atm if not filename.endswith('.json.gz'): filename += '.json.gz' @@ -472,7 +464,7 @@ def myip(ipv6): @click.option('--fields', help='List of properties to show in the search results.', default='ip_str,port,hostnames,data') @click.option('--limit', help='The number of search results that should be returned. Maximum: 1000', default=100, type=int) @click.option('--separator', help='The separator between the properties of the search results.', default='\t') -@click.argument('query', metavar='', nargs=-1) +@click.argument('query', metavar='', nargs=-1, callback=check_not_null) def search(color, fields, limit, separator, query): """Search the Shodan database""" key = get_api_key() @@ -480,10 +472,6 @@ def search(color, fields, limit, separator, query): # Create the query string out of the provided tuple query = ' '.join(query).strip() - # Make sure the user didn't supply an empty string - if query == '': - raise click.ClickException('Empty search query') - # For now we only allow up to 1000 results at a time if limit > 1000: raise click.ClickException('Too many results requested, maximum is 1,000') @@ -543,7 +531,7 @@ def search(color, fields, limit, separator, query): @click.option('--limit', help='The number of results to return.', default=10, type=int) @click.option('--facets', help='List of facets to get statistics for.', default='country,org') @click.option('--filename', '-O', help='Save the results in a CSV file of the provided name.', default=None) -@click.argument('query', metavar='', nargs=-1) +@click.argument('query', metavar='', nargs=-1, callback=check_not_null) def stats(limit, facets, filename, query): """Provide summary information about a search query""" # Setup Shodan @@ -810,7 +798,7 @@ def _create_stream(name, args, timeout): @click.option('--facets', help='List of facets to get summary information on, if empty then show query total results over time', default='', type=str) @click.option('--filename', '-O', help='Save the full results in the given file (append if file exists).', default=None) @click.option('--save', '-S', help='Save the full results in the a file named after the query (append if file exists).', default=False, is_flag=True) -@click.argument('query', metavar='', nargs=-1) +@click.argument('query', metavar='', nargs=-1, callback=check_not_null) def trends(filename, save, facets, query): """Search Shodan historical database""" key = get_api_key() @@ -820,10 +808,6 @@ def trends(filename, save, facets, query): query = ' '.join(query).strip() facets = facets.strip() - # Make sure the user didn't supply an empty query or facets - if query == '': - raise click.ClickException('Empty search query') - # Convert comma-separated facets string to list parsed_facets = [] for facet in facets.split(','): diff --git a/shodan/cli/validation.py b/shodan/cli/validation.py index da53b90..183e37a 100644 --- a/shodan/cli/validation.py +++ b/shodan/cli/validation.py @@ -1,4 +1,17 @@ import click +from os import path + + +def check_not_null(ctx, param, value): + """ + Click callback method used to verify command line parameter is not an empty string. + :param ctx: Python Click library Context object. + :param param: Python Click Context object params attribute. + :param value: Value passed in for a given command line parameter. + """ + if not value: + raise click.BadParameter("Value cannot be empty / null") + return value def check_input_file_type(ctx, param, value): @@ -13,3 +26,24 @@ def check_input_file_type(ctx, param, value): if idx == -1 or value[idx:] != ".json.gz": raise click.BadParameter("Input file type must be '.json.gz'") return value + + +def check_filename_filepath(ctx, param, value): + """ + Click callback method used for file path input validation. + :param ctx: Python Click library Context object. + :param param: Python Click Context object params attribute. + :param value: Value passed in for a given command line parameter. + """ + filename = value.strip() + folder_idx = filename.rfind('/') + + if filename == '': + raise click.click.BadParameter('Empty filename') + + if folder_idx != -1: + parent_folder = filename[0: folder_idx + 1] + if not path.exists(parent_folder): + raise click.BadParameter('File path does not exist.') + + return value