diff --git a/Integrations/ApiShell/README.md b/Integrations/ApiShell/README.md index 301b256..08153f3 100644 --- a/Integrations/ApiShell/README.md +++ b/Integrations/ApiShell/README.md @@ -8,9 +8,9 @@ entries useful for making IriusRisk API calls. Most of the command line arguments provided can be duplicated in configuration files, thereby allowing you to provide them in a file once across multiple script calls. -For further information, clone this subdirectory and execute the main file: +For further information, clone this subdirectory and execute the following: - python3 main.py + python3 -c 'import iriusrisk.v1' --help This describes in greater detail how to call the program shell. diff --git a/Integrations/ApiShell/iriusrisk/__init__.py b/Integrations/ApiShell/iriusrisk/__init__.py new file mode 100644 index 0000000..eebf21c --- /dev/null +++ b/Integrations/ApiShell/iriusrisk/__init__.py @@ -0,0 +1,133 @@ +"""This module provides helper methods for parsing the command line and +loading configuration files. Call --help on the command line to get detailed +information regarding which arguments are expected on the comand line and in +the configuration files. + +Besides initial configuration, this module contains the sub-module "facade," +which itself provides helper methods for calling the IriusRisk API. + +The configuration values are contained in an object named "config," which is +imported by default. The base configuration contains raw command line and +configuration file parameters, plus the attribute "url," which contains the +full URL where the API is being accessed. +""" +import iriusrisk.commandline +import iriusrisk.configfile +import http.client +import json +import logging +import sys + +__all__=["config,parse_arguments,get_commandline_parser"] + +config = None +parse_args_on_load = True + +_log = logging.getLogger('iriusrisk') +_parser = iriusrisk.commandline.get_parser() + +"""Call this method before loading any sub-modules of iriusrisk. This then +prevents the automatic parsing of the configuration files and command line, +which is normally done following initialization of the sub-modules. +""" +def suppress_parse_args_on_load(): + parse_args_on_load = False + +"""This returns the instance of the argparse class used by this method. Use +this to add needed command line parameters prior to calling parse_arguments(). +""" +def get_commandline_parser(): + return _parser + +"""Parse the command line arguments, and load in any config files. By default, +this method is called after any sub-module of iriusrisk is initialized. +""" +def parse_arguments(): + global config + config = _parser.parse_args() + + if config.verbose: + logging.basicConfig(level=logging.DEBUG) + elif config.quiet: + logging.basicConfig(level=logging.ERROR) + + _raw_config = iriusrisk.configfile.parse_config() + + if not len(sys.argv) > 1 and not _raw_config: + _parser.print_help() + exit(-1) + + if not _raw_config: + _log.info("No configuration file (iriusrisk.ini) found in any of the locations.") + _log.info("See help (--help) for more information.") + _raw_config = { "DEFAULT":{}} + elif "DEFAULT" in _raw_config: + _log.info("Successfully parsed at least one configuration file") + else: + _log.warn("At least one configuration file was read in, but no [DEFAULT] section was found.") + _log.info("All configurations in the file(s) will therefore be ignored. See help (--help)") + _log.info("for further information") + + _log.info("Starting configuration initialization") + + config.key = _get_item(_raw_config, config.key, "key", None) + + if not config.key: + _log.error("No --key has been specified. Any API call will fail. See help (--help) for more information.") + + if config.dryrun: + _log.info("Option --dryrun passed on the command line. No HTTP calls will be made.") + + config.url = _get_url(_raw_config) + _check_url(config.url) + +def _get_url(config_file): + full_url = _get_item(config_file, config.full_url, "full-url", None) + if full_url: + _log.info("Using the --full-url option. No URL will be derived from domain or subdomain.") + return full_url + + subdomain = _get_item(config_file, config.subdomain, "subdomain", None) + if subdomain: + _log.info("Using the --subdomain option. URL will be derived by appending .iriusrisk.com (SaaS instance).") + return f"{subdomain}.iriusrisk.com:443" + + domain = _get_item(config_file, config.domain, "domain", None) + + if domain: + _log.info("Using the --domain option. Protocol assumed to be HTTPS") + return "{domain}:443" + +def _get_item(config_file, value, key, default_value): + if value: + _log.debug(f"Found {key} on the command line. Will take precedence over config file") + return value + + if key in config_file["DEFAULT"]: + _log.debug(f"Found {key} in a configuration file but not on the command line.") + return config_file["DEFAULT"][key] + + _log.debug(f"Item {key} not found in configuration file or on the command line.") + return default_value + +def _check_url(url): + if not url: + _log.error("No URL has been specified!") + _log.warn("You must supply one of subdomain, domain or url on the command line or in the ini file") + _log.warn("Get extended help (--help) from the program for more information") + + if not config.dryrun: + _log.info("Making a call to the given URL as a fail-fast test") + _log.debug("Note that this does not test the security key's validity, but just whether") + _log.debug("the URL is valid and accepting requests.") + headers = { "accept": "application/json" } + conn = http.client.HTTPSConnection(url) + conn.request("GET", "/health", None, headers) + resp = conn.getresponse() + if resp.status != 200: + _log.error(f"Call to {url} returned status of {resp.status} ({resp.reason}); program will exit") + exit(-1) + + data = resp.read().decode("utf-8") + json_obj = json.loads(data) + _log.debug(f"Successfully checked health of {url} (server: {json_obj['company']})") \ No newline at end of file diff --git a/Integrations/ApiShell/iriusrisk/v1/commandline.py b/Integrations/ApiShell/iriusrisk/commandline.py similarity index 50% rename from Integrations/ApiShell/iriusrisk/v1/commandline.py rename to Integrations/ApiShell/iriusrisk/commandline.py index dbe0913..d2c48ad 100644 --- a/Integrations/ApiShell/iriusrisk/v1/commandline.py +++ b/Integrations/ApiShell/iriusrisk/commandline.py @@ -16,9 +16,9 @@ def get_parser(): create more complex scripts that create, call and process API calls. Using this shell program provides default command-line and configuration-file -entries useful for making IriusRisk API calls. Most of the command line -arguments provided can be duplicated in configuration files, thereby allowing -you to providethem in a file once across multiple script calls. +entries useful for making IriusRisk API calls. Some of the command line +arguments can appear in configuration files, thereby allowing them to be +specified across multiple script calls. Configuration files are always named "iriusrisk.ini" and are searched for in the following locations: @@ -27,19 +27,15 @@ def get_parser(): * {user data directory}/.iriusrisk/iriusrisk.ini * {global data directory}/.iriusrisk/iriusrisk.ini -There is a precedence for the ini files; the first one overrides, the next, and -so on. Hence, the same keys may appear in multiple ini files, but the one -"nearest" to the script will be used. - -Command line arguments take precedence over ini files. The verbosity parameters -(verbose and quiet) are only available on the command line. - +The same keys may appear in multiple ini files, but the one "nearest" to the +script will be used. For instance, if the same key appears in the ini file +in the working directory and the in the user data directory, the one in the +working directory takes precedence. +Command line arguments take precedence over ini files. """), epilog=textwrap.dedent( """*************************************************** -Format and contents of the initialization files - Each ini file contains a default namespace that must be explicitly declared. Within this namespace are zero or more key/value pairs, corresponding to the command line arguments. Format is as follows: @@ -53,41 +49,11 @@ def get_parser(): [DEFAULT] key = a123b456-78c9-01d2-e345-f6789ab012c3 - -A note regarding URLs: -When accessing a HTTP server, Python expects a URL that contains the domain and -port number, but no protocol. For most users, the complexity of this can be -ignored completely. For instance, if you are accessing a IriusRisk instance -provided as SaaS by IriusRisk, you only need to provide the --subdomain of the -instance. For instance, if your IriusRisk instance has a URL as follows: - - https://my-company.iriusrisk.com/ - -you need only provide the subdomain parameter on the command line or in a config -file: - - --subdomain my-company - -If instead, you have a non-IriusRisk domain hosting your instance, you can -specify this using the domain parameter: - - --domain ir-subdomain.example.com - -Finally, the url parameter can be used for URLs that don't follow this simple -pattern. Note that when using the url parameter, you may not include a -protocol (e.g., https), but you must supply the port number. For instance: - - --url subdomain.example.com:443/iriusrisk - -The port parameter can be used when leveraging the domain parameter. It defaults -to 443. Only one of the parameters subdomain, domain and url are recognized. """ )) - parser.add_argument("-k", "--key", help="API Token to use when accessing the API") parser.add_argument("-s", "--subdomain", help="Subdomain of a SaaS instance. Will be prepended to .iriusrisk.com") parser.add_argument("-d", "--domain", help="The entire domain of the target system, without protocol or path.") - parser.add_argument("-p", "--port", help="Defaults to 443.") - parser.add_argument("-u", "--url", help="Enter the entire base URL for the target system. Cannot have protocol, but must have port.") + parser.add_argument("-f", "--full-url", help="The target system's complete URL, port number included, but no protocol.") parser.add_argument("-v", "--verbose", help="Output extended log information", action='store_true') parser.add_argument("-q", "--quiet", help="Only output log messages indicating errors", action='store_true') parser.add_argument("--dryrun", help="Do everything but actual HTTP calls", action='store_true') diff --git a/Integrations/ApiShell/iriusrisk/v1/configfile.py b/Integrations/ApiShell/iriusrisk/configfile.py similarity index 100% rename from Integrations/ApiShell/iriusrisk/v1/configfile.py rename to Integrations/ApiShell/iriusrisk/configfile.py diff --git a/Integrations/ApiShell/iriusrisk/v1/__init__.py b/Integrations/ApiShell/iriusrisk/v1/__init__.py index fc83b71..1704b5c 100644 --- a/Integrations/ApiShell/iriusrisk/v1/__init__.py +++ b/Integrations/ApiShell/iriusrisk/v1/__init__.py @@ -1,148 +1,12 @@ """This module provides helper methods for accessing IriusRisk API version v1. -It automatically parses the command line and searches for configuration files. -Call --help on the command line to get detailed information regarding which -config attributes are expected on the comand line and in the configuration -files, as well as other details regarding configuration prioritization. +It also adds the --key command line parser, which expects the IriusRisk v1 API +key as it's argument. -Besides initial configuration, this module contains the sub-module "facade," -which itself provides helper methods for calling the IriusRisk API. - -The configuration values are contained in an object named "config," which is -imported by default. All configuration items are attributes of that object -and reflect the name of the attribute as defined. So for instance, typical -attributes are as follows: - -config: - url : the URL, calculated or defined, to the IriusRisk instance - key : the API key required to access the instance - dryrun : whether actual remote calls should be made or not - verbose : whether verbose output should be included - quiet : whether output should only be minimal or not - -There may be other attributes, as well. For instance, while subdomain is a -valid application parameter, it is not a required one. It will only be an -attribute of config if it is actually passed, either as an applicaiton -parameter or within a configuration file. +Specific methods for calling into the API are provided in the iriusrisk.v1.facade +module. """ -import logging -import http.client -import json -import sys - -import iriusrisk.v1.commandline as _commandline -import iriusrisk.v1.configfile as _configfile - -# Automatically loaded and executed when this module is loaded. -# -# This script automatically parses the command line and any configuration files found. -# It also determines the ultimate URL that will be used for calling IriusRisk. - -__all__=["config"] - -class _configuration: - key = None - url = None - verbose = False - quiet = False - dryrun = False - -config = _configuration() -_log = logging.getLogger('iriusrisk.v1') -_parser = _commandline.get_parser() -_parsed_args = () - -def _get_item(value, key, default_value=None): - if value: - _log.debug(f"Found {key} on the command line. Will take precedence over config file") - return value - - if key in _raw_config["DEFAULT"]: - _log.debug(f"Found {key} in a configuration file but not on the command line.") - return _raw_config["DEFAULT"][key] - - _log.debug(f"Item {key} not found in configuration file or on the command line.") - return default_value - -def get_url(): - port = _get_item(_parsed_args.port, "port", "443") - url = _get_item(_parsed_args.url, "url") - if url: - _log.info("Using the --url option. No URL will be derived from domain or subdomain.") - return url - - subdomain = _get_item(_parsed_args.subdomain, "subdomain") - if subdomain: - _log.info("Using the --subdomain option. URL will be derived by appending .iriusrisk.com (SaaS instance).") - return f"{subdomain}.iriusrisk.com:{port}" - - domain = _get_item(_parsed_args.domain, "domain") - - if domain: - _log.info("Using the --domain option. Protocol assumed to be HTTPS") - return "{domain}:{port}" - -def check_url(url): - if not url: - _log.error("No URL has been specified!") - _log.warn("You must supply one of subdomain, domain or url on the command line or in the ini file") - _log.warn("Get extended help (--help) from the program for more information") - - if not config.dryrun: - _log.info("Making a call to the given URL as a fail-fast test") - _log.debug("Note that this does not test the security key's validity, but just whether") - _log.debug("the URL is valid and accepting requests.") - headers = { "accept": "application/json" } - conn = http.client.HTTPSConnection(url) - conn.request("GET", "/health", None, headers) - resp = conn.getresponse() - if resp.status != 200: - _log.error(f"Call to {url} returned status of {resp.status} ({resp.reason}); program will exit") - exit(-1) - - data = resp.read().decode("utf-8") - json_obj = json.loads(data) - _log.debug(f"Successfully checked health of {url} (server: {json_obj['company']})") - -### Start reading the config files and command line args -_parser = _commandline.get_parser() - -_parsed_args = _parser.parse_args() - -if _parsed_args.verbose: - config.verbose = True - logging.basicConfig(level=logging.DEBUG) -elif _parsed_args.quiet: - config.quiet = True - logging.basicConfig(level=logging.ERROR) - -_raw_config = _configfile.parse_config() - -if not len(sys.argv) > 1 and not _raw_config: - _parser.print_help() - exit(-1) - -if not _raw_config: - _log.info("No configuration file (iriusrisk.ini) found in any of the locations.") - _log.info("See help (--help) for more information.") - _raw_config = { "DEFAULT":{}} -elif "DEFAULT" in _raw_config: - _log.info("Successfully parsed at least one configuration file") -else: - _log.warn("At least one configuration file was read in, but no [DEFAULT] section was found.") - _log.info("All configurations in the file(s) will therefore be ignored. See help (--help)") - _log.info("for further information") - -_log.info("Starting configuration initialization") - -config.key = _get_item(_parsed_args.key, "key") - -if not config.key: - _log.error("No --key has been specified. Any API call will fail. See help (--help) for more information.") - -config.dryrun = _parsed_args.dryrun - -if config.dryrun: - _log.info("Option --dryrun passed on the command line. No HTTP calls will be made.") +import iriusrisk -config.url = get_url() -check_url(config.url) \ No newline at end of file +iriusrisk.get_commandline_parser().add_argument("-k", "--key", help="API Token to use when accessing the API") +if (iriusrisk.parse_args_on_load): + iriusrisk.parse_arguments() \ No newline at end of file diff --git a/Integrations/ApiShell/iriusrisk/v1/facade.py b/Integrations/ApiShell/iriusrisk/v1/facade.py index 4735259..12d67de 100644 --- a/Integrations/ApiShell/iriusrisk/v1/facade.py +++ b/Integrations/ApiShell/iriusrisk/v1/facade.py @@ -5,7 +5,7 @@ import json import logging from urllib.parse import quote -from iriusrisk.v1 import * +from iriusrisk import config # Provides helper methods that make accessing the IriusRisk v1 API easier. _log = logging.getLogger('iriusrisk.v1') @@ -42,11 +42,11 @@ def call_endpoint(path, verb, headers={}, params={}, convert_response=True, enco encode_path : (default: False): whether URL encoding should be applied to the various elements of the path. - The method returns a tuple containing (in order) the HTTP response, and JSON - returned as the body of the response. The second element of the tuple is None - if convert_response == False. - - See the Python json module for information regarding the returned JSON object. + The method returns a tuple containing the HTTP response and data returned as the body + of the response. The type of the data depends on two things. First, if convert_response + is False, plain text is returned. Otherwise, it depends on the return type of the + API call. If (for instance) the return type is "application/json," then a json object + is returned. """ path = _build_path(path, encode_path) _log.info(f"Calling endpoint {path} with verb {verb}") @@ -80,6 +80,8 @@ def call_endpoint(path, verb, headers={}, params={}, convert_response=True, enco return (resp, result) +"""Call the specified endpoint using "GET." +""" def do_get(path, headers={}, params={}, convert_response=True, encode_path=False): """Call the indicated endpoint via GET. See call_endpoint for more details.""" return call_endpoint(path, "GET", headers, params, convert_response, encode_path) \ No newline at end of file diff --git a/Integrations/ApiShell/main.py b/Integrations/ApiShell/main.py index e2c20e5..8dee474 100644 --- a/Integrations/ApiShell/main.py +++ b/Integrations/ApiShell/main.py @@ -1,12 +1,8 @@ import logging -from iriusrisk.v1 import * +import iriusrisk.v1 # importing this reads initialization files and parses the command line +from iriusrisk import config -# This import does a few things automatically: -# * it parses the command line -# * it attempts to read one or more configuration files -# * all configurations from the command line and configuration files are available on the config variable -# -# For further information to the possible parameters, execute this file with the --help parameter, -# and call help(iriusrisk.v1) or help(iriusrisk.v1.facade). +logging.getLogger(__name__).warning(f"Accessing IriusRisk via the URL {config.url}") -logging.getLogger(__name__).warning(f"Accessing IriusRisk via the URL {config.url}") \ No newline at end of file +help(iriusrisk) +help(iriusrisk.v1) \ No newline at end of file