diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e566bce9..578c9f488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 The intended audience of this file is for py42 consumers -- as such, changes that don't affect how a consumer would use the library (e.g. adding unit tests, updating documentation, etc) are not captured here. -## Unreleased +## 0.4.0 - 2020-03-12 + +### Added + +- Support for multiple profiles: + - Optional `--profile` flag for: + - `securitydata write-to`, `print`, and `send-to`, + - `profile show`, `set`, and `reset-pw`. + - `code42 profile use` command for changing the default profile. + - `code42 profile list` command for listing all the available profiles. +- The following search args can now take multiple values: + - `--c42username`, + - `--actor`, + - `--md5`, + - `--sha256`, + - `--filename`, + - `--filepath`, + - `--processOwner`, + - `--tabURL` ### Fixed @@ -18,6 +36,7 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta - Begin dates are no longer required for subsequent interactive `securitydata` commands. - When provided, begin dates are now ignored on subsequent interactive `securitydata` commands. +- `--profile` arg is now required the first time setting up a profile. ## 0.3.0 - 2020-03-04 diff --git a/README.md b/README.md index 176414287..de4f76fec 100644 --- a/README.md +++ b/README.md @@ -21,16 +21,20 @@ $ python setup.py install First, set your profile: ```bash -code42 profile set -s https://example.authority.com -u security.admin@example.com +code42 profile set --profile MY_FIRST_PROFILE -s https://example.authority.com -u security.admin@example.com ``` +The `--profile` flag is required the first time and it takes a name. +On subsequent uses of `set`, not specifying the profile will set the default profile. + Your profile contains the necessary properties for logging into Code42 servers. -After running this `code42 profile set`, you will be prompted about storing a password. -If you agree, you will be securely prompted to input your password. -Your password is not stored in plain-text, and is not shown when you do `code42 profile show`. -However, `code42 profile show` will confirm that there is a password set for your profile. +After running `code42 profile set`, the program prompts you about storing a password. +If you agree, you are then prompted to input your password. + +Your password is not stored in plain-text and is not shown when you do `code42 profile show`. +However, `code42 profile show` will confirm that a password exists for your profile. If you do not set a password, you will be securely prompted to enter a password each time you run a command. -To ignore SSL errors, do: +For development purposes, you may need to ignore ssl errors. If you need to do this, do: ```bash code42 profile set --disable-ssl-errors ``` @@ -40,7 +44,19 @@ To re-enable SSL errors, do: code42 profile set --enable-ssl-errors ``` -Next, you can query for events and send them to three possible destination types +You can add multiple profiles with different names and the change the default profile with the `use` command: +```bash +code42 profile use MY_SECOND_PROFILE +``` +When the `--profile` flag is available on other commands, such as those in `securitydata`, +it will use that profile instead of the default one. + +To see all your profiles, do: +```bash +code42 profile list +``` + +Using the CLI, you can query for events and send them to three possible destination types: * stdout * A file * A server, such as SysLog @@ -58,6 +74,12 @@ code42 securitydata print -b 2020-02-02 12:51 ``` Begin date will be ignored if provided on subsequent queries using `-i`. +Use different format with `-f`: +```bash +code42 securitydata print -b 2020-02-02 -f CEF +``` +The available formats are CEF, JSON, and RAW-JSON. + To write events to a file, do: ```bash code42 securitydata write-to filename.txt -b 2020-02-02 @@ -74,6 +96,16 @@ code42 securitydata send-to syslog.company.com -i ``` This is only guaranteed if you did not change your query. +To send events to a server using a specific profile, do: +```bash +code42 securitydata send-to --profile PROFILE_FOR_RECURRING_JOB syslog.company.com -b 2020-02-02 -f CEF -i +``` + +You can also use wildcard for queries, but note, if they are not in quotes, you may get unexpected behavior. +```bash +code42 securitydata print --actor "*" +``` + Each destination-type subcommand shares query parameters * `-t` (exposure types) @@ -92,7 +124,7 @@ Each destination-type subcommand shares query parameters * `--advanced-query` (raw JSON query) You cannot use other query parameters if you use `--advanced-query`. -To learn more about acceptable arguments, add the `-h` flag to `code42` or and of the destination-type subcommands. +To learn more about acceptable arguments, add the `-h` flag to `code42` or any of the destination-type subcommands. # Known Issues diff --git a/src/code42cli/__version__.py b/src/code42cli/__version__.py index 493f7415d..6a9beea82 100644 --- a/src/code42cli/__version__.py +++ b/src/code42cli/__version__.py @@ -1 +1 @@ -__version__ = "0.3.0" +__version__ = "0.4.0" diff --git a/src/code42cli/arguments.py b/src/code42cli/arguments.py new file mode 100644 index 000000000..e7f4d4f83 --- /dev/null +++ b/src/code42cli/arguments.py @@ -0,0 +1,25 @@ +PROFILE_NAME_KEY = u"profile_name" +PROFILE_HELP_MESSAGE = ( + u"The name of the profile containing your Code42 username and authority host address." +) + + +def add_arguments_to_parser(parser): + add_debug_arg(parser) + add_profile_name_arg(parser) + + +def add_debug_arg(parser): + parser.add_argument( + u"-d", + u"--debug", + dest=u"is_debug_mode", + action=u"store_true", + help=u"Turn on Debug logging.", + ) + + +def add_profile_name_arg(parser): + parser.add_argument( + u"--profile", action=u"store", dest=PROFILE_NAME_KEY, help=PROFILE_HELP_MESSAGE + ) diff --git a/src/code42cli/compat.py b/src/code42cli/compat.py index 3972f6f2c..9a94559a9 100644 --- a/src/code42cli/compat.py +++ b/src/code42cli/compat.py @@ -15,7 +15,11 @@ from urlparse import urljoin, urlparse str = unicode + + import repr as reprlib else: from urllib.parse import urljoin, urlparse str = str + + import reprlib diff --git a/src/code42cli/main.py b/src/code42cli/main.py index 548f75e7f..7926e0479 100644 --- a/src/code42cli/main.py +++ b/src/code42cli/main.py @@ -34,3 +34,7 @@ def _run(parser): parser.print_help() return raise ex + + +if __name__ == "__main__": + main() diff --git a/src/code42cli/profile/config.py b/src/code42cli/profile/config.py index 57121add7..660feca76 100644 --- a/src/code42cli/profile/config.py +++ b/src/code42cli/profile/config.py @@ -6,136 +6,137 @@ import code42cli.util as util from code42cli.compat import str -_DEFAULT_VALUE = u"__DEFAULT__" - -class ConfigurationKeys(object): - USER_SECTION = u"Code42" +class ConfigAccessor(object): + DEFAULT_VALUE = u"__DEFAULT__" AUTHORITY_KEY = u"c42_authority_url" USERNAME_KEY = u"c42_username" IGNORE_SSL_ERRORS_KEY = u"ignore-ssl-errors" - INTERNAL_SECTION = u"Internal" - HAS_SET_PROFILE_KEY = u"has_set_profile" - - -def get_config_profile(): - """Get your config file profile.""" - parser = ConfigParser() - if not profile_has_been_set(): - util.print_error(u"Profile has not completed setup.") - print(u"") - print(u"To set, use: ") - util.print_bold(u"\tcode42 profile set -s -u ") - print(u"") - exit(1) - - return _get_config_profile_from_parser(parser) - - -def profile_has_been_set(): - """Whether you have, at one point in time, set your username and authority server URL.""" - parser = ConfigParser() - config_file_path = _get_config_file_path() - parser.read(config_file_path) - settings = parser[ConfigurationKeys.INTERNAL_SECTION] - return settings.getboolean(ConfigurationKeys.HAS_SET_PROFILE_KEY) - - -def mark_as_set_if_complete(): - if not _profile_can_be_set(): - return - parser = ConfigParser() - config_file_path = _get_config_file_path() - parser.read(config_file_path) - settings = parser[ConfigurationKeys.INTERNAL_SECTION] - settings[ConfigurationKeys.HAS_SET_PROFILE_KEY] = u"True" - _save(parser, ConfigurationKeys.HAS_SET_PROFILE_KEY) - - -def set_username(new_username): - parser = ConfigParser() - profile = _get_config_profile_from_parser(parser) - profile[ConfigurationKeys.USERNAME_KEY] = new_username - _save(parser, ConfigurationKeys.USERNAME_KEY) - - -def set_authority_url(new_url): - parser = ConfigParser() - profile = _get_config_profile_from_parser(parser) - profile[ConfigurationKeys.AUTHORITY_KEY] = new_url - _save(parser, ConfigurationKeys.AUTHORITY_KEY) - - -def set_ignore_ssl_errors(new_value): - parser = ConfigParser() - profile = _get_config_profile_from_parser(parser) - profile[ConfigurationKeys.IGNORE_SSL_ERRORS_KEY] = str(new_value) - _save(parser, ConfigurationKeys.IGNORE_SSL_ERRORS_KEY) - - -def _profile_can_be_set(): - """Whether your current username and authority URL are set, - but your profile has not been marked as set. - """ - parser = ConfigParser() - profile = _get_config_profile_from_parser(parser) - username = profile[ConfigurationKeys.USERNAME_KEY] - authority = profile[ConfigurationKeys.AUTHORITY_KEY] - return username != _DEFAULT_VALUE and authority != _DEFAULT_VALUE and not profile_has_been_set() - - -def _get_config_profile_from_parser(parser): - config_file_path = _get_config_file_path() - parser.read(config_file_path) - config = parser[ConfigurationKeys.USER_SECTION] - return config - - -def _get_config_file_path(): - path = u"{}config.cfg".format(util.get_user_project_path()) - if not os.path.exists(path) or not _verify_config_file(path): - _create_new_config_file(path) - return path - - -def _create_new_config_file(path): - config_parser = ConfigParser() - config_parser = _create_user_section(config_parser) - config_parser = _create_internal_section(config_parser) - _save(config_parser, None, path) - - -def _create_user_section(parser): - keys = ConfigurationKeys - parser.add_section(keys.USER_SECTION) - parser[keys.USER_SECTION] = {} - parser[keys.USER_SECTION][keys.AUTHORITY_KEY] = _DEFAULT_VALUE - parser[keys.USER_SECTION][keys.USERNAME_KEY] = _DEFAULT_VALUE - parser[keys.USER_SECTION][keys.IGNORE_SSL_ERRORS_KEY] = u"False" - return parser - - -def _create_internal_section(parser): - keys = ConfigurationKeys - parser.add_section(keys.INTERNAL_SECTION) - parser[keys.INTERNAL_SECTION] = {} - parser[keys.INTERNAL_SECTION][keys.HAS_SET_PROFILE_KEY] = u"False" - return parser - - -def _save(parser, key=None, path=None): - path = _get_config_file_path() if path is None else path - util.open_file(path, u"w+", lambda f: parser.write(f)) - if key is not None: - if key == ConfigurationKeys.HAS_SET_PROFILE_KEY: - print(u"You have completed setting up your profile!") + DEFAULT_PROFILE_IS_COMPLETE = u"default_profile_is_complete" + DEFAULT_PROFILE = u"default_profile" + _INTERNAL_SECTION = u"Internal" + + def __init__(self, parser): + self.parser = parser + self.path = u"{}config.cfg".format(util.get_user_project_path()) + if not os.path.exists(self.path): + self._create_internal_section() + self._save() else: - print(u"'{}' has been successfully updated".format(key)) - - -def _verify_config_file(path): - keys = ConfigurationKeys - config_parser = ConfigParser() - config_parser.read(path) - sections = config_parser.sections() - return keys.USER_SECTION in sections and keys.INTERNAL_SECTION in sections + self.parser.read(self.path) + + def get_profile(self, name=None): + """Returns the profile with the given name. + If name is None, returns the default profile. + If the name does not exist or there is no existing profile, it will throw an exception. + """ + name = name or self._default_profile_name + if name not in self.parser.sections() or name == self.DEFAULT_VALUE: + raise Exception(u"Profile does not exist.") + return self.parser[name] + + def get_all_profiles(self): + """Returns all the available profiles.""" + profiles = [] + names = self._get_profile_names() + for name in names: + profiles.append(self.get_profile(name)) + return profiles + + def create_profile_if_not_exists(self, name): + """Creates a new profile if one does not already exist for that name.""" + try: + self.get_profile(name) + except Exception as ex: + if name is not None and name != self.DEFAULT_VALUE: + self._create_profile_section(name) + else: + raise ex + + def switch_default_profile(self, new_default_name): + """Changes what is marked as the default profile in the internal section.""" + if self.get_profile(new_default_name) is None: + raise Exception(u"Profile does not exist.") + self._internal[self.DEFAULT_PROFILE] = new_default_name + self._save() + + def set_authority_url(self, new_value, profile_name=None): + """Sets 'authority URL' for a given profile. + Uses the default profile if name is None. + """ + profile = self.get_profile(profile_name) + profile[self.AUTHORITY_KEY] = new_value.strip() + self._save() + self._try_complete_setup(profile) + + def set_username(self, new_value, profile_name=None): + """Sets 'username' for a given profile. Uses the default profile if not given a name.""" + profile = self.get_profile(profile_name) + profile[self.USERNAME_KEY] = new_value.strip() + self._save() + self._try_complete_setup(profile) + + def set_ignore_ssl_errors(self, new_value, profile_name=None): + """Sets 'ignore_ssl_errors' for a given profile. + Uses the default profile if name is None. + """ + profile = self.get_profile(profile_name) + profile[self.IGNORE_SSL_ERRORS_KEY] = str(new_value) + self._save() + + @property + def _internal(self): + """The internal section of the config file.""" + return self.parser[self._INTERNAL_SECTION] + + @property + def _default_profile_name(self): + return self._internal[self.DEFAULT_PROFILE] + + def _get_profile_names(self): + names = list(self.parser.sections()) + names.remove(self._INTERNAL_SECTION) + return names + + def _create_internal_section(self): + self.parser.add_section(self._INTERNAL_SECTION) + self.parser[self._INTERNAL_SECTION] = {} + self.parser[self._INTERNAL_SECTION][self.DEFAULT_PROFILE_IS_COMPLETE] = str(False) + self.parser[self._INTERNAL_SECTION][self.DEFAULT_PROFILE] = self.DEFAULT_VALUE + + def _create_profile_section(self, name): + self.parser.add_section(name) + self.parser[name] = {} + self.parser[name][self.AUTHORITY_KEY] = self.DEFAULT_VALUE + self.parser[name][self.USERNAME_KEY] = self.DEFAULT_VALUE + self.parser[name][self.IGNORE_SSL_ERRORS_KEY] = str(False) + default_profile = self._internal.get(self.DEFAULT_PROFILE) + if default_profile is None or default_profile is self.DEFAULT_VALUE: + self._internal[self.DEFAULT_PROFILE] = name + + def _save(self): + util.open_file(self.path, u"w+", lambda f: self.parser.write(f)) + + def _try_complete_setup(self, profile): + if self._internal.getboolean(self.DEFAULT_PROFILE_IS_COMPLETE): + return + + authority = profile.get(self.AUTHORITY_KEY) + username = profile.get(self.USERNAME_KEY) + + authority_valid = authority and authority != self.DEFAULT_VALUE + username_valid = username and username != self.DEFAULT_VALUE + + if not authority_valid or not username_valid: + return + + self._internal[self.DEFAULT_PROFILE_IS_COMPLETE] = str(True) + if self._internal[self.DEFAULT_PROFILE] == self.DEFAULT_VALUE: + self._internal[self.DEFAULT_PROFILE] = profile.name + + self._save() + + +def get_config_accessor(): + """Create a ConfigAccessor with a ConfigParser as its parser.""" + return ConfigAccessor(ConfigParser()) diff --git a/src/code42cli/profile/password.py b/src/code42cli/profile/password.py index 7b79937c4..b08c85b0e 100644 --- a/src/code42cli/profile/password.py +++ b/src/code42cli/profile/password.py @@ -4,26 +4,27 @@ import keyring -import code42cli.profile.config as config -from code42cli.profile.config import ConfigurationKeys +from code42cli.profile.config import get_config_accessor, ConfigAccessor _ROOT_SERVICE_NAME = u"code42cli" -def get_password(): - """Gets your currently stored password for your username / authority URL combo.""" - profile = config.get_config_profile() - service_name = _get_service_name(profile) +def get_password(profile_name): + """Gets your currently stored password for your profile.""" + accessor = get_config_accessor() + profile = accessor.get_profile(profile_name) + service_name = _get_service_name(profile_name) username = _get_username(profile) password = keyring.get_password(service_name, username) return password -def set_password_from_prompt(): - """Prompts and sets your password for your username / authority URL combo.""" +def set_password_from_prompt(profile_name): + """Prompts and sets your password for your profile.""" password = getpass() - profile = config.get_config_profile() - service_name = _get_service_name(profile) + accessor = get_config_accessor() + profile = accessor.get_profile(profile_name) + service_name = _get_service_name(profile_name) username = _get_username(profile) keyring.set_password(service_name, username, password) print(u"'Code42 Password' updated.") @@ -34,10 +35,9 @@ def get_password_from_prompt(): return getpass() -def _get_service_name(profile): - authority_url = profile[ConfigurationKeys.AUTHORITY_KEY] - return u"{}::{}".format(_ROOT_SERVICE_NAME, authority_url) +def _get_service_name(profile_name): + return u"{}::{}".format(_ROOT_SERVICE_NAME, profile_name) def _get_username(profile): - return profile[ConfigurationKeys.USERNAME_KEY] + return profile[ConfigAccessor.USERNAME_KEY] diff --git a/src/code42cli/profile/profile.py b/src/code42cli/profile/profile.py index 4c1520b89..2c06d0082 100644 --- a/src/code42cli/profile/profile.py +++ b/src/code42cli/profile/profile.py @@ -1,23 +1,48 @@ from __future__ import print_function -import code42cli.profile.config as config +import code42cli.arguments as main_args import code42cli.profile.password as password -from code42cli.profile.config import ConfigurationKeys -from code42cli.util import get_input +from code42cli.compat import str +from code42cli.profile.config import get_config_accessor, ConfigAccessor +from code42cli.util import ( + get_input, + print_error, + print_set_profile_help, + print_no_existing_profile_message, +) class Code42Profile(object): - authority_url = u"" - username = u"" - ignore_ssl_errors = False + def __init__(self, profile): + self._profile = profile - @staticmethod - def get_password(): - pwd = password.get_password() + @property + def name(self): + return self._profile.name + + @property + def authority_url(self): + return self._profile[ConfigAccessor.AUTHORITY_KEY] + + @property + def username(self): + return self._profile[ConfigAccessor.USERNAME_KEY] + + @property + def ignore_ssl_error(self): + return self._profile[ConfigAccessor.IGNORE_SSL_ERRORS_KEY] + + def get_password(self): + pwd = password.get_password(self.name) if not pwd: pwd = password.get_password_from_prompt() return pwd + def __str__(self): + return u"{0}: Username={1}, Authority URL={2}".format( + self.name, self.username, self.authority_url + ) + def init(subcommand_parser): """Sets up the `profile` subcommand with `show` and `set` subcommands. @@ -26,61 +51,101 @@ def init(subcommand_parser): Args: subcommand_parser: The subparsers group created by the parent parser. """ - parser_profile = subcommand_parser.add_parser("profile") - parser_profile.set_defaults(func=show_profile) + parser_profile = subcommand_parser.add_parser(u"profile") profile_subparsers = parser_profile.add_subparsers() - parser_for_show_command = profile_subparsers.add_parser("show") - parser_for_set_command = profile_subparsers.add_parser("set") - parser_for_reset_password = profile_subparsers.add_parser("reset-pw") + parser_for_show = profile_subparsers.add_parser(u"show") + parser_for_set = profile_subparsers.add_parser(u"set") + parser_for_reset_password = profile_subparsers.add_parser(u"reset-pw") + parser_for_list = profile_subparsers.add_parser(u"list") + parser_for_use = profile_subparsers.add_parser(u"use") - parser_for_show_command.set_defaults(func=show_profile) - parser_for_set_command.set_defaults(func=set_profile) + parser_for_show.set_defaults(func=show_profile) + parser_for_set.set_defaults(func=set_profile) parser_for_reset_password.set_defaults(func=prompt_for_password_reset) - _add_args_to_set_command(parser_for_set_command) + parser_for_list.set_defaults(func=list_profiles) + parser_for_use.set_defaults(func=use_profile) + + main_args.add_profile_name_arg(parser_for_show) + main_args.add_profile_name_arg(parser_for_reset_password) + _add_args_to_set_command(parser_for_set) + _add_positional_profile_arg(parser_for_use) + + +def get_profile(profile_name=None): + """Returns the profile for the given name.""" + accessor = get_config_accessor() + try: + profile = accessor.get_profile(profile_name) + return Code42Profile(profile) + except Exception as ex: + print_error(str(ex)) + print_set_profile_help() + exit(1) + + +def show_profile(args): + """Prints the given profile to stdout.""" + profile = get_profile(args.profile_name) + print(u"\n{0}:".format(profile.name)) + print(u"\t* {0} = {1}".format(ConfigAccessor.USERNAME_KEY, profile.username)) + print(u"\t* {0} = {1}".format(ConfigAccessor.AUTHORITY_KEY, profile.authority_url)) + print(u"\t* {0} = {1}".format(ConfigAccessor.IGNORE_SSL_ERRORS_KEY, profile.ignore_ssl_error)) + if password.get_password(args.profile_name) is not None: + print(u"\t* A password is set.") + print(u"") -def get_profile(): - """Returns the current profile object.""" - profile_values = config.get_config_profile() - profile = Code42Profile() - profile.authority_url = profile_values.get(ConfigurationKeys.AUTHORITY_KEY) - profile.username = profile_values.get(ConfigurationKeys.USERNAME_KEY) - profile.ignore_ssl_errors = profile_values.get(ConfigurationKeys.IGNORE_SSL_ERRORS_KEY) - return profile +def set_profile(args): + """Sets the given profile using command line arguments.""" + _verify_args_for_set(args) + accessor = get_config_accessor() + accessor.create_profile_if_not_exists(args.profile_name) + _try_set_authority_url(args, accessor) + _try_set_username(args, accessor) + _try_set_ignore_ssl_errors(args, accessor) + _prompt_for_allow_password_set(args) -def show_profile(*args): - """Prints the current profile to stdout.""" - profile = config.get_config_profile() - print(u"\nProfile:") - for key in profile: - print(u"\t* {} = {}".format(key, profile[key])) +def prompt_for_password_reset(args): + """Securely prompts for your password and then stores it using keyring.""" + password.set_password_from_prompt(args.profile_name) - if password.get_password() is not None: - print(u"\t* A password is set.") - print(u"") +def list_profiles(*args): + """Lists all profiles that exist for this OS user.""" + accessor = get_config_accessor() + profiles = accessor.get_all_profiles() + if not profiles: + print_no_existing_profile_message() + return + for profile in profiles: + profile = Code42Profile(profile) + print(profile) -def set_profile(args): - """Sets the current profile using command line arguments.""" - _try_set_authority_url(args) - _try_set_username(args) - _try_set_ignore_ssl_errors(args) - config.mark_as_set_if_complete() - _prompt_for_allow_password_set() +def use_profile(args): + """Changes the default profile to the given one.""" + accessor = get_config_accessor() + try: + accessor.switch_default_profile(args.profile_name) + except Exception as ex: + print_error(ex) + exit(1) -def prompt_for_password_reset(*args): - """Securely prompts for your password and then stores it using keyring.""" - password.set_password_from_prompt() + +def _add_args_to_set_command(parser_for_set): + main_args.add_profile_name_arg(parser_for_set) + _add_authority_arg(parser_for_set) + _add_username_arg(parser_for_set) + _add_disable_ssl_errors_arg(parser_for_set) + _add_enable_ssl_errors_arg(parser_for_set) -def _add_args_to_set_command(parser_for_set_command): - _add_authority_arg(parser_for_set_command) - _add_username_arg(parser_for_set_command) - _add_disable_ssl_errors_arg(parser_for_set_command) - _add_enable_ssl_errors_arg(parser_for_set_command) +def _add_positional_profile_arg(parser): + parser.add_argument( + action=u"store", dest=main_args.PROFILE_NAME_KEY, help=main_args.PROFILE_HELP_MESSAGE + ) def _add_authority_arg(parser): @@ -88,7 +153,7 @@ def _add_authority_arg(parser): u"-s", u"--server", action=u"store", - dest=ConfigurationKeys.AUTHORITY_KEY, + dest=ConfigAccessor.AUTHORITY_KEY, help=u"The full scheme, url and port of the Code42 server.", ) @@ -98,7 +163,7 @@ def _add_username_arg(parser): u"-u", u"--username", action=u"store", - dest=ConfigurationKeys.USERNAME_KEY, + dest=ConfigAccessor.USERNAME_KEY, help=u"The username of the Code42 API user.", ) @@ -109,7 +174,8 @@ def _add_disable_ssl_errors_arg(parser): action=u"store_true", default=None, dest=u"disable_ssl_errors", - help=u"Do not validate the SSL certificates of Code42 servers.", + help=u"For development purposes, do not validate the SSL certificates of Code42 servers." + u"This is not recommended unless it is required.", ) @@ -123,33 +189,68 @@ def _add_enable_ssl_errors_arg(parser): ) -def _set_has_args(args): - return args.c42_authority_url is not None or args.c42_username is not None +def _verify_args_for_set(args): + if _missing_default_profile(args): + print_error(u"Must supply a name when setting your profile for the first time.") + print_set_profile_help() + exit(1) + + missing_values = not args.c42_username and not args.c42_authority_url + if missing_values: + try: + profile = get_profile(args.profile_name) + missing_values = not profile.username and not profile.authority_url + except SystemExit: + missing_values = True + + if missing_values: + print_error(u"Missing username and authority url.") + print_set_profile_help() + exit(1) -def _try_set_authority_url(args): +def _try_set_authority_url(args, accessor): if args.c42_authority_url is not None: - config.set_authority_url(args.c42_authority_url) + accessor.set_authority_url(args.c42_authority_url, args.profile_name) -def _try_set_username(args): +def _try_set_username(args, accessor): if args.c42_username is not None: - config.set_username(args.c42_username) + accessor.set_username(args.c42_username, args.profile_name) -def _try_set_ignore_ssl_errors(args): +def _try_set_ignore_ssl_errors(args, accessor): if args.disable_ssl_errors is not None and not args.enable_ssl_errors: - config.set_ignore_ssl_errors(True) + accessor.set_ignore_ssl_errors(True, args.profile_name) if args.enable_ssl_errors is not None: - config.set_ignore_ssl_errors(False) + accessor.set_ignore_ssl_errors(False, args.profile_name) + + +def _missing_default_profile(args): + profile_name_arg_is_none = ( + args.profile_name is None or args.profile_name == ConfigAccessor.DEFAULT_VALUE + ) + return profile_name_arg_is_none and not _default_profile_exists() + + +def _default_profile_exists(): + try: + accessor = get_config_accessor() + profile = Code42Profile(accessor.get_profile()) + return profile.name and profile.name != ConfigAccessor.DEFAULT_VALUE + except Exception: + return False -def _prompt_for_allow_password_set(): +def _prompt_for_allow_password_set(args): answer = get_input(u"Would you like to set a password? (y/n): ") if answer.lower() == u"y": - prompt_for_password_reset() + prompt_for_password_reset(args) -if __name__ == "__main__": - show_profile() +def _log_key_save(key): + if key == ConfigAccessor.DEFAULT_PROFILE_IS_COMPLETE: + print(u"You have completed setting up your profile!") + else: + print(u"'{}' has been successfully updated".format(key)) diff --git a/src/code42cli/securitydata/arguments/main.py b/src/code42cli/securitydata/arguments/main.py index 11608344f..f3fa0d0f7 100644 --- a/src/code42cli/securitydata/arguments/main.py +++ b/src/code42cli/securitydata/arguments/main.py @@ -7,7 +7,6 @@ def add_arguments_to_parser(parser): _add_output_format_arg(parser) _add_incremental_arg(parser) - _add_debug_arg(parser) def _add_output_format_arg(parser): @@ -30,13 +29,3 @@ def _add_incremental_arg(parser): action=u"store_true", help=u"Only get events that were not previously retrieved.", ) - - -def _add_debug_arg(parser): - parser.add_argument( - u"-d", - u"--debug", - dest=u"is_debug_mode", - action=u"store_true", - help=u"Turn on Debug logging.", - ) diff --git a/src/code42cli/securitydata/arguments/search.py b/src/code42cli/securitydata/arguments/search.py index c686bac18..b10c2a071 100644 --- a/src/code42cli/securitydata/arguments/search.py +++ b/src/code42cli/securitydata/arguments/search.py @@ -23,15 +23,15 @@ class SearchArguments(object): BEGIN_DATE = u"begin_date" END_DATE = u"end_date" EXPOSURE_TYPES = u"exposure_types" - C42USERNAME = u"c42username" - ACTOR = u"actor" - MD5 = u"md5" - SHA256 = u"sha256" - SOURCE = u"source" - FILENAME = u"filename" - FILEPATH = u"filepath" - PROCESS_OWNER = u"process_owner" - TAB_URL = u"tab_url" + C42USERNAME = u"c42usernames" + ACTOR = u"actors" + MD5 = u"md5_hashes" + SHA256 = u"sha256_hashes" + SOURCE = u"sources" + FILENAME = u"filenames" + FILEPATH = u"filepaths" + PROCESS_OWNER = u"process_owners" + TAB_URL = u"tab_urls" INCLUDE_NON_EXPOSURE_EVENTS = u"include_non_exposure_events" def __iter__(self): @@ -105,72 +105,80 @@ def _add_exposure_types_arg(parser): def _add_username_arg(parser): parser.add_argument( u"--c42username", + nargs=u"+", action=u"store", dest=SearchArguments.C42USERNAME, - help=u"Limits events to endpoint events for this user.", + help=u"Limits events to endpoint events for these users.", ) def _add_actor_arg(parser): parser.add_argument( u"--actor", + nargs=u"+", action=u"store", dest=SearchArguments.ACTOR, - help=u"Limits events to only those enacted by this actor.", + help=u"Limits events to only those enacted by these actors.", ) def _add_md5_arg(parser): parser.add_argument( u"--md5", + nargs=u"+", action=u"store", dest=SearchArguments.MD5, - help=u"Limits events to file events where the file has this MD5 hash.", + help=u"Limits events to file events where the file has one of these MD5 hashes.", ) def _add_sha256_arg(parser): parser.add_argument( u"--sha256", + nargs=u"+", action=u"store", dest=SearchArguments.SHA256, - help=u"Limits events to file events where the file has this SHA256 hash.", + help=u"Limits events to file events where the file has one of these SHA256 hashes.", ) def _add_source_arg(parser): parser.add_argument( u"--source", + nargs=u"+", action=u"store", dest=SearchArguments.SOURCE, - help=u"Limits events to only those from this source. Example=Gmail.", + help=u"Limits events to only those from one of these sources. Example=Gmail.", ) def _add_filename_arg(parser): parser.add_argument( u"--filename", + nargs=u"+", action=u"store", dest=SearchArguments.FILENAME, - help=u"Limits events to file events where the file has this name.", + help=u"Limits events to file events where the file has one of these names.", ) def _add_filepath_arg(parser): parser.add_argument( u"--filepath", + nargs=u"+", action=u"store", dest=SearchArguments.FILEPATH, - help=u"Limits events to file events where the file is located at this path.", + help=u"Limits events to file events where the file is located at one of these paths.", ) def _add_process_owner_arg(parser): parser.add_argument( u"--processOwner", + nargs=u"+", action=u"store", dest=SearchArguments.PROCESS_OWNER, - help=u"Limits events to exposure events where this user " + help=u"Limits events to exposure events where one of these users " u"owns the process behind the exposure.", ) @@ -178,9 +186,10 @@ def _add_process_owner_arg(parser): def _add_tab_url_arg(parser): parser.add_argument( u"--tabURL", + nargs=u"+", action=u"store", dest=SearchArguments.TAB_URL, - help=u"Limits events to be exposure events with this destination tab URL.", + help=u"Limits events to be exposure events with one of these destination tab URLs.", ) diff --git a/src/code42cli/securitydata/cursor_store.py b/src/code42cli/securitydata/cursor_store.py index 5ecb87fa2..b3db2726f 100644 --- a/src/code42cli/securitydata/cursor_store.py +++ b/src/code42cli/securitydata/cursor_store.py @@ -33,6 +33,17 @@ def _set(self, column_name, new_value, primary_key): with self._connection as conn: conn.execute(query, (new_value, primary_key)) + def _row_exists(self, primary_key): + query = u"SELECT * FROM {0} WHERE {1}=?" + query = query.format(self._table_name, self._PRIMARY_KEY_COLUMN_NAME) + with self._connection as conn: + cursor = conn.cursor() + cursor.execute(query, (primary_key,)) + query_result = cursor.fetchone() + if not query_result: + return False + return True + def _drop_table(self): drop_query = u"DROP TABLE {0}".format(self._table_name) with self._connection as conn: @@ -53,16 +64,17 @@ def _is_empty(self): class FileEventCursorStore(BaseCursorStore): - _PRIMARY_KEY = 1 - - def __init__(self, db_file_path=None): - super(FileEventCursorStore, self).__init__(u"aed_checkpoint", db_file_path) + def __init__(self, profile_name, db_file_path=None): + self._primary_key = profile_name + super(FileEventCursorStore, self).__init__(u"file_event_checkpoints", db_file_path) if self._is_empty(): self._init_table() + if not self._row_exists(self._primary_key): + self._insert_new_row() def get_stored_insertion_timestamp(self): """Gets the last stored insertion timestamp.""" - rows = self._get(_INSERTION_TIMESTAMP_FIELD_NAME, self._PRIMARY_KEY) + rows = self._get(_INSERTION_TIMESTAMP_FIELD_NAME, self._primary_key) if rows and rows[0]: return rows[0][0] @@ -71,17 +83,16 @@ def replace_stored_insertion_timestamp(self, new_insertion_timestamp): self._set( column_name=_INSERTION_TIMESTAMP_FIELD_NAME, new_value=new_insertion_timestamp, - primary_key=self._PRIMARY_KEY, + primary_key=self._primary_key, ) - def reset(self): - self._drop_table() - self._init_table() - def _init_table(self): columns = u"{0}, {1}".format(self._PRIMARY_KEY_COLUMN_NAME, _INSERTION_TIMESTAMP_FIELD_NAME) create_table_query = u"CREATE TABLE {0} ({1})".format(self._table_name, columns) - insert_query = u"INSERT INTO {0} VALUES(?, null)".format(self._table_name) with self._connection as conn: conn.execute(create_table_query) - conn.execute(insert_query, (self._PRIMARY_KEY,)) + + def _insert_new_row(self): + insert_query = u"INSERT INTO {0} VALUES(?, null)".format(self._table_name) + with self._connection as conn: + conn.execute(insert_query, (self._primary_key,)) diff --git a/src/code42cli/securitydata/date_helper.py b/src/code42cli/securitydata/date_helper.py index 7856bce36..07fd85900 100644 --- a/src/code42cli/securitydata/date_helper.py +++ b/src/code42cli/securitydata/date_helper.py @@ -14,7 +14,6 @@ def create_event_timestamp_filter(begin_date=None, end_date=None): Args: begin_date: The begin date for the range. end_date: The end date for the range. - """ end_date = _get_end_date_with_eod_time_if_needed(end_date) if begin_date and end_date: diff --git a/src/code42cli/securitydata/extraction.py b/src/code42cli/securitydata/extraction.py index 67903a779..ed2a95d06 100644 --- a/src/code42cli/securitydata/extraction.py +++ b/src/code42cli/securitydata/extraction.py @@ -37,68 +37,28 @@ def extract(output_logger, args): args: Command line args used to build up file event query filters. """ - store = _create_cursor_store(args) + profile = get_profile(args.profile_name) + store = _create_cursor_store(args, profile) + filters = _get_filters(args, store) handlers = _create_event_handlers(output_logger, store) - profile = get_profile() sdk = _get_sdk(profile, args.is_debug_mode) extractor = FileEventExtractor(sdk, handlers) - _call_extract(extractor, store, args) + _call_extract(extractor, filters, args) _handle_result() -def _create_cursor_store(args): +def _create_cursor_store(args, profile): if args.is_incremental: - return FileEventCursorStore() + return FileEventCursorStore(profile.name) -def _create_event_handlers(output_logger, cursor_store): - handlers = FileEventHandlers() - error_logger = get_error_logger() - - def handle_error(exception): - error_logger.error(exception) - global _EXCEPTIONS_OCCURRED - _EXCEPTIONS_OCCURRED = True - - handlers.handle_error = handle_error - - if cursor_store: - handlers.record_cursor_position = cursor_store.replace_stored_insertion_timestamp - handlers.get_cursor_position = cursor_store.get_stored_insertion_timestamp - - def handle_response(response): - response_dict = json.loads(response.text) - events = response_dict.get(u"fileEvents") - for event in events: - output_logger.info(event) - - handlers.handle_response = handle_response - return handlers - - -def _get_sdk(profile, is_debug_mode): - if is_debug_mode: - settings.debug_level = debug_level.DEBUG - try: - return SDK.create_using_local_account( - profile.authority_url, profile.username, profile.get_password() - ) - except: - print_error( - u"Invalid credentials or host address. " - u"Verify your profile is set up correctly and that you are supplying the correct password." - ) - exit(1) - - -def _call_extract(extractor, cursor_store, args): +def _get_filters(args, cursor_store): if not _determine_if_advanced_query(args): _verify_begin_date_requirements(args, cursor_store) _verify_exposure_types(args.exposure_types) - filters = _create_filters(args) - extractor.extract(*filters) + return _create_filters(args) else: - extractor.extract_advanced(args.advanced_query) + return args.advanced_query def _determine_if_advanced_query(args): @@ -113,14 +73,6 @@ def _determine_if_advanced_query(args): return False -def _verify_compatibility_with_advanced_query(key, val): - if val is not None: - is_other_search_arg = key in SearchArguments() and key != SearchArguments.ADVANCED_QUERY - is_incremental = key == IS_INCREMENTAL_KEY and val - return not is_other_search_arg and not is_incremental - return True - - def _verify_begin_date_requirements(args, cursor_store): if _begin_date_is_required(args, cursor_store) and not args.begin_date: print_error(u"'begin date' is required.") @@ -151,6 +103,23 @@ def _verify_exposure_types(exposure_types): exit(1) +def _create_filters(args): + filters = [] + event_timestamp_filter = _get_event_timestamp_filter(args) + not event_timestamp_filter or filters.append(event_timestamp_filter) + not args.c42usernames or filters.append(DeviceUsername.is_in(args.c42usernames)) + not args.actors or filters.append(Actor.is_in(args.actors)) + not args.md5_hashes or filters.append(MD5.is_in(args.md5_hashes)) + not args.sha256_hashes or filters.append(SHA256.is_in(args.sha256_hashes)) + not args.sources or filters.append(Source.is_in(args.sources)) + not args.filenames or filters.append(FileName.is_in(args.filenames)) + not args.filepaths or filters.append(FilePath.is_in(args.filepaths)) + not args.process_owners or filters.append(ProcessOwner.is_in(args.process_owners)) + not args.tab_urls or filters.append(TabURL.is_in(args.tab_urls)) + _try_append_exposure_types_filter(filters, args) + return filters + + def _get_event_timestamp_filter(args): try: return date_helper.create_event_timestamp_filter(args.begin_date, args.end_date) @@ -159,30 +128,68 @@ def _get_event_timestamp_filter(args): exit(1) +def _create_event_handlers(output_logger, cursor_store): + handlers = FileEventHandlers() + error_logger = get_error_logger() + + def handle_error(exception): + error_logger.error(exception) + global _EXCEPTIONS_OCCURRED + _EXCEPTIONS_OCCURRED = True + + handlers.handle_error = handle_error + + if cursor_store: + handlers.record_cursor_position = cursor_store.replace_stored_insertion_timestamp + handlers.get_cursor_position = cursor_store.get_stored_insertion_timestamp + + def handle_response(response): + response_dict = json.loads(response.text) + events = response_dict.get(u"fileEvents") + for event in events: + output_logger.info(event) + + handlers.handle_response = handle_response + return handlers + + +def _get_sdk(profile, is_debug_mode): + if is_debug_mode: + settings.debug_level = debug_level.DEBUG + try: + password = profile.get_password() + return SDK.create_using_local_account(profile.authority_url, profile.username, password) + except Exception: + print_error( + u"Invalid credentials or host address. " + u"Verify your profile is set up correctly and that you are supplying the correct password." + ) + exit(1) + + +def _call_extract(extractor, filters, args): + if args.advanced_query: + extractor.extract_advanced(args.advanced_query) + else: + extractor.extract(*filters) + + +def _verify_compatibility_with_advanced_query(key, val): + if key == SearchArguments.INCLUDE_NON_EXPOSURE_EVENTS and not val: + return True + + if val is not None: + is_other_search_arg = key in SearchArguments() and key != SearchArguments.ADVANCED_QUERY + is_incremental = key == IS_INCREMENTAL_KEY and val + return not is_other_search_arg and not is_incremental + return True + + def _handle_result(): if is_interactive() and _EXCEPTIONS_OCCURRED: print_error(u"View exceptions that occurred at [HOME]/.code42cli/log/code42_errors.") -def _create_filters(args): - filters = [] - event_timestamp_filter = _get_event_timestamp_filter(args) - if event_timestamp_filter: - filters.append(event_timestamp_filter) - - not args.c42username or filters.append(DeviceUsername.eq(args.c42username)) - not args.actor or filters.append(Actor.eq(args.actor)) - not args.md5 or filters.append(MD5.eq(args.md5)) - not args.sha256 or filters.append(SHA256.eq(args.sha256)) - not args.source or filters.append(Source.eq(args.source)) - not args.filename or filters.append(FileName.eq(args.filename)) - not args.filepath or filters.append(FilePath.eq(args.filepath)) - not args.process_owner or filters.append(ProcessOwner.eq(args.process_owner)) - not args.tab_url or filters.append(TabURL.eq(args.tab_url)) - _try_append_exposure_types_filter(filters, args) - return filters - - def _try_append_exposure_types_filter(filters, args): exposure_filter = _create_exposure_type_filter(args) if exposure_filter: diff --git a/src/code42cli/securitydata/subcommands/clear_checkpoint.py b/src/code42cli/securitydata/subcommands/clear_checkpoint.py index d4961309a..b59d04eed 100644 --- a/src/code42cli/securitydata/subcommands/clear_checkpoint.py +++ b/src/code42cli/securitydata/subcommands/clear_checkpoint.py @@ -1,3 +1,5 @@ +from code42cli.arguments import add_profile_name_arg +from code42cli.profile.profile import get_profile from code42cli.securitydata.cursor_store import FileEventCursorStore @@ -7,16 +9,14 @@ def init(subcommand_parser): subcommand_parser: The subparsers group created by the parent parser. """ parser = subcommand_parser.add_parser("clear-checkpoint") + add_profile_name_arg(parser) parser.set_defaults(func=clear_checkpoint) -def clear_checkpoint(*args): +def clear_checkpoint(args): """Removes the stored checkpoint that keeps track of the last event you got. To use, run `code42 clear-checkpoint`. This affects `incremental` mode by causing it to behave like it has never been run before. """ - FileEventCursorStore().reset() - - -if __name__ == "__main__": - clear_checkpoint() + profile_name = args.profile_name or get_profile().name + FileEventCursorStore(profile_name).replace_stored_insertion_timestamp(None) diff --git a/src/code42cli/securitydata/subcommands/print_out.py b/src/code42cli/securitydata/subcommands/print_out.py index 7642f4037..cf10c8185 100644 --- a/src/code42cli/securitydata/subcommands/print_out.py +++ b/src/code42cli/securitydata/subcommands/print_out.py @@ -1,4 +1,5 @@ -from code42cli.securitydata.arguments import main as main_args +import code42cli.arguments as main_args +from code42cli.securitydata.arguments import main as securitydata_main_args from code42cli.securitydata.arguments import search as search_args from code42cli.securitydata.extraction import extract from code42cli.securitydata.logger_factory import get_logger_for_stdout @@ -13,6 +14,7 @@ def init(subcommand_parser): parser = subcommand_parser.add_parser("print") parser.set_defaults(func=print_out) search_args.add_arguments_to_parser(parser) + securitydata_main_args.add_arguments_to_parser(parser) main_args.add_arguments_to_parser(parser) diff --git a/src/code42cli/securitydata/subcommands/send_to.py b/src/code42cli/securitydata/subcommands/send_to.py index fd2d28d26..592c3416e 100644 --- a/src/code42cli/securitydata/subcommands/send_to.py +++ b/src/code42cli/securitydata/subcommands/send_to.py @@ -1,4 +1,5 @@ -from code42cli.securitydata.arguments import main as main_args +import code42cli.arguments as main_args +from code42cli.securitydata.arguments import main as securitydata_main_args from code42cli.securitydata.arguments import search as search_args from code42cli.securitydata.extraction import extract from code42cli.securitydata.logger_factory import get_logger_for_server @@ -16,6 +17,7 @@ def init(subcommand_parser): _add_server_arg(parser) _add_protocol_arg(parser) search_args.add_arguments_to_parser(parser) + securitydata_main_args.add_arguments_to_parser(parser) main_args.add_arguments_to_parser(parser) diff --git a/src/code42cli/securitydata/subcommands/write_to.py b/src/code42cli/securitydata/subcommands/write_to.py index 543dee68c..e190bc783 100644 --- a/src/code42cli/securitydata/subcommands/write_to.py +++ b/src/code42cli/securitydata/subcommands/write_to.py @@ -1,4 +1,5 @@ -from code42cli.securitydata.arguments import main as main_args +import code42cli.arguments as main_args +from code42cli.securitydata.arguments import main as securitydata_main_args from code42cli.securitydata.arguments import search as search_args from code42cli.securitydata.extraction import extract from code42cli.securitydata.logger_factory import get_logger_for_file @@ -14,6 +15,7 @@ def init(subcommand_parser): parser.set_defaults(func=write_to) _add_filename_subcommand(parser) search_args.add_arguments_to_parser(parser) + securitydata_main_args.add_arguments_to_parser(parser) main_args.add_arguments_to_parser(parser) diff --git a/src/code42cli/util.py b/src/code42cli/util.py index 9e4964292..ae107ea91 100644 --- a/src/code42cli/util.py +++ b/src/code42cli/util.py @@ -3,8 +3,6 @@ import sys from os import path, makedirs -from code42cli.compat import urlparse - def get_input(prompt): """Uses correct input function based on Python version.""" @@ -35,7 +33,7 @@ def open_file(file_path, mode, action): def print_error(error_text): """Prints red text.""" - print("\033[91mERROR: {}\033[0m".format(error_text)) + print("\033[91mUSAGE ERROR: {}\033[0m".format(error_text)) def print_bold(bold_text): @@ -46,6 +44,18 @@ def is_interactive(): return sys.stdin.isatty() +def print_no_existing_profile_message(): + print_error(u"No existing profile.") + print_set_profile_help() + + +def print_set_profile_help(): + print(u"") + print(u"To add a profile, use: ") + print_bold(u"\tcode42 profile set --profile -s -u ") + print(u"") + + def get_url_parts(url_str): parts = url_str.split(u":") port = None diff --git a/tests/conftest.py b/tests/conftest.py index 98f7d1a73..7c3df3e6b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,64 +4,30 @@ import pytest -from code42cli.profile.config import ConfigurationKeys - - -@pytest.fixture -def config_profile(mocker): - mock_config = mocker.patch("code42cli.profile.config.get_config_profile") - mock_config.return_value = { - ConfigurationKeys.USERNAME_KEY: "test.username", - ConfigurationKeys.AUTHORITY_KEY: "https://authority.example.com", - ConfigurationKeys.IGNORE_SSL_ERRORS_KEY: "True", - } - return mock_config - - -@pytest.fixture -def config_parser(mocker): - mocks = ConfigParserMocks() - mocks.initializer = mocker.patch("configparser.ConfigParser.__init__") - mocks.item_setter = mocker.patch("configparser.ConfigParser.__setitem__") - mocks.item_getter = mocker.patch("configparser.ConfigParser.__getitem__") - mocks.section_adder = mocker.patch("configparser.ConfigParser.add_section") - mocks.reader = mocker.patch("configparser.ConfigParser.read") - mocks.sections = mocker.patch("configparser.ConfigParser.sections") - mocks.initializer.return_value = None - return mocks - @pytest.fixture def namespace(mocker): mock = mocker.MagicMock(spec=Namespace) + mock.profile_name = None mock.is_incremental = None mock.advanced_query = None mock.is_debug_mode = None mock.begin_date = None mock.end_date = None mock.exposure_types = None - mock.c42username = None - mock.actor = None - mock.md5 = None - mock.sha256 = None - mock.source = None - mock.filename = None - mock.filepath = None - mock.process_owner = None - mock.tab_url = None + mock.c42usernames = None + mock.actors = None + mock.md5_hashes = None + mock.sha256_hashes = None + mock.sources = None + mock.filenames = None + mock.filepaths = None + mock.process_owners = None + mock.tab_urls = None mock.include_non_exposure_events = None return mock -class ConfigParserMocks(object): - initializer = None - item_setter = None - item_getter = None - section_adder = None - reader = None - sections = None - - def get_filter_value_from_json(json, filter_index): return json_module.loads(str(json))["filters"][filter_index]["value"] diff --git a/tests/profile/test_config.py b/tests/profile/test_config.py index e206e43c6..f895c9515 100644 --- a/tests/profile/test_config.py +++ b/tests/profile/test_config.py @@ -1,141 +1,203 @@ from __future__ import with_statement -import pytest - -import code42cli.profile.config as config - - -class SharedConfigMocks(object): - mocker = None - open_function = None - path_exists_function = None - get_project_path_function = None - config_parser = None - - def setup_existing_config_file(self): - self.path_exists_function.return_value = True - sections = self.mocker.patch("configparser.ConfigParser.sections") - sections.return_value = [ - config.ConfigurationKeys.INTERNAL_SECTION, - config.ConfigurationKeys.USER_SECTION, - ] +from configparser import ConfigParser - def setup_non_existing_config_file(self): - self.path_exists_function.return_value = False - - def setup_existing_profile(self): - self.config_parser.item_getter.return_value = self._create_config_profile(is_set=True) - - def setup_non_existing_profile(self): - self.config_parser.item_getter.return_value = self._create_config_profile(is_set=False) +import pytest - def _create_config_profile(self, is_set): - config_profile = self.mocker.MagicMock() - config_profile.getboolean.return_value = is_set - bool_getter = self.mocker.MagicMock() - bool_getter.return_value = is_set - config_profile.getboolean = bool_getter - config_profile.__setitem__ = self.mocker.MagicMock() - return config_profile +from code42cli.profile.config import ConfigAccessor @pytest.fixture -def shared_config_mocks(mocker, config_parser): - # Project path - get_project_path_function = mocker.patch("code42cli.util.get_user_project_path") - get_project_path_function.return_value = "some/path/" - - # Opening files - open_file_function = mocker.patch("code42cli.util.open_file") - new_file = mocker.MagicMock() - open_file_function.return_value = new_file - - # Path exists - path_exists_function = mocker.patch("os.path.exists") - - mocks = SharedConfigMocks() - mocks.mocker = mocker - mocks.open_function = open_file_function - mocks.path_exists_function = path_exists_function - mocks.get_project_path_function = get_project_path_function - mocks.config_parser = config_parser - return mocks - - -def save_was_called(open_file_function): - call_args = open_file_function.call_args - try: - return call_args[0][0] == "some/path/config.cfg" and call_args[0][1] == "w+" - except: - return False - - -def test_get_config_profile_when_file_exists_but_profile_does_not_exist_exits(shared_config_mocks): - shared_config_mocks.setup_existing_config_file() - shared_config_mocks.setup_non_existing_profile() - - # It is expected to exit because the user must set their profile before they can see it. - with pytest.raises(SystemExit): - config.get_config_profile() - - -def test_get_config_profile_when_file_exists_and_profile_is_set_does_not_exit(shared_config_mocks): - shared_config_mocks.setup_existing_config_file() - shared_config_mocks.setup_existing_profile() - - # Presumably, it shows the profile instead of exiting. - assert config.get_config_profile() - - -def test_get_config_profile_when_file_does_not_exist_saves_changes(shared_config_mocks): - shared_config_mocks.setup_non_existing_config_file() - shared_config_mocks.setup_non_existing_profile() - - with pytest.raises(SystemExit): - config.get_config_profile() - - # It saves because it is writing default values to the config file - assert save_was_called(shared_config_mocks.open_function) - - -def test_profile_has_been_set_when_is_set_returns_true(shared_config_mocks): - shared_config_mocks.setup_existing_profile() - assert config.profile_has_been_set() - - -def test_profile_has_been_set_when_is_not_set_returns_false(shared_config_mocks): - shared_config_mocks.setup_non_existing_profile() - assert not config.profile_has_been_set() - - -def test_mark_as_set_if_complete_when_profile_is_set_but_not_marked_in_config_file_saves( - shared_config_mocks -): - shared_config_mocks.setup_existing_profile() - shared_config_mocks.setup_non_existing_config_file() - config.mark_as_set_if_complete() - assert save_was_called(shared_config_mocks.open_function) - - -def test_mark_as_set_if_complete_when_already_set_and_marked_in_config_file_does_not_save( - shared_config_mocks -): - shared_config_mocks.setup_existing_profile() - shared_config_mocks.setup_existing_config_file() - config.mark_as_set_if_complete() - assert not save_was_called(shared_config_mocks.open_function) - - -def test_set_username_saves(shared_config_mocks): - config.set_username("New user") - assert save_was_called(shared_config_mocks.open_function) - - -def test_set_authority_url_saves(shared_config_mocks): - config.set_authority_url("New url") - assert save_was_called(shared_config_mocks.open_function) - +def mock_config_parser(mocker): + return mocker.MagicMock(sepc=ConfigParser) -def test_set_ignore_ssl_errors_saves(shared_config_mocks): - config.set_ignore_ssl_errors(True) - assert save_was_called(shared_config_mocks.open_function) + +@pytest.fixture(autouse=True) +def mock_saver(mocker): + return mocker.patch("code42cli.util.open_file") + + +def create_mock_profile_object(name, authority=None, username=None): + authority = authority or ConfigAccessor.DEFAULT_VALUE + username = username or ConfigAccessor.DEFAULT_VALUE + profile_dict = {ConfigAccessor.AUTHORITY_KEY: authority, ConfigAccessor.USERNAME_KEY: username} + + class ProfileObject(object): + def __getitem__(self, item): + return profile_dict[item] + + def __setitem__(self, key, value): + profile_dict[key] = value + + def get(self, item): + return profile_dict.get(item) + + @property + def name(self): + return name + + return ProfileObject() + + +def create_internal_object(is_complete, default_profile_name=None): + default_profile_name = default_profile_name or ConfigAccessor.DEFAULT_VALUE + internal_dict = { + ConfigAccessor.DEFAULT_PROFILE: default_profile_name, + ConfigAccessor.DEFAULT_PROFILE_IS_COMPLETE: is_complete, + } + + class InternalObject(object): + def __getitem__(self, item): + return internal_dict[item] + + def __setitem__(self, key, value): + internal_dict[key] = value + + def getboolean(self, *args): + return is_complete + + return InternalObject() + + +def setup_parser_one_profile(profile, internal, parser): + def side_effect(item): + if item == "ProfileA": + return profile + elif item == "Internal": + return internal + + parser.__getitem__.side_effect = side_effect + + +class TestConfigAccessor(object): + def test_get_profile_when_profile_does_not_exist_raises(self, mock_config_parser): + mock_config_parser.sections.return_value = ["Internal"] + accessor = ConfigAccessor(mock_config_parser) + with pytest.raises(Exception): + accessor.get_profile("Profile Name that does not exist") + + def test_get_profile_when_profile_has_default_name_raises(self, mock_config_parser): + mock_config_parser.sections.return_value = ["Internal"] + accessor = ConfigAccessor(mock_config_parser) + with pytest.raises(Exception): + accessor.get_profile("__DEFAULT__") + + def test_get_profile_returns_expected_profile(self, mock_config_parser): + mock_config_parser.sections.return_value = ["Internal", "ProfileA"] + accessor = ConfigAccessor(mock_config_parser) + accessor.get_profile("ProfileA") + assert mock_config_parser.__getitem__.call_args[0][0] == "ProfileA" + + def test_get_all_profiles_excludes_internal_section(self, mock_config_parser): + mock_config_parser.sections.return_value = ["ProfileA", "Internal", "ProfileB"] + accessor = ConfigAccessor(mock_config_parser) + profiles = accessor.get_all_profiles() + for p in profiles: + if p.name == "Internal": + assert False + + def test_set_username_marks_as_complete_if_ready(self, mock_config_parser): + mock_config_parser.sections.return_value = ["Internal", "ProfileA"] + accessor = ConfigAccessor(mock_config_parser) + mock_profile = create_mock_profile_object("ProfileA", "example.com", None) + mock_internal = create_internal_object(False) + setup_parser_one_profile(mock_profile, mock_internal, mock_config_parser) + accessor.set_username("TestUser", "ProfileA") + assert mock_internal[ConfigAccessor.DEFAULT_PROFILE] == "ProfileA" + assert mock_internal[ConfigAccessor.DEFAULT_PROFILE_IS_COMPLETE] + + def test_set_username_does_not_mark_as_complete_if_not_have_authority(self, mock_config_parser): + mock_config_parser.sections.return_value = ["Internal", "ProfileA"] + accessor = ConfigAccessor(mock_config_parser) + mock_profile = create_mock_profile_object("ProfileA", None, None) + mock_internal = create_internal_object(False) + setup_parser_one_profile(mock_profile, mock_internal, mock_config_parser) + accessor.set_username("TestUser", "ProfileA") + assert mock_internal[ConfigAccessor.DEFAULT_PROFILE] == ConfigAccessor.DEFAULT_VALUE + assert not mock_internal[ConfigAccessor.DEFAULT_PROFILE_IS_COMPLETE] + + def test_set_username_saves(self, mock_config_parser, mock_saver): + mock_config_parser.sections.return_value = ["Internal", "ProfileA"] + accessor = ConfigAccessor(mock_config_parser) + mock_profile = create_mock_profile_object("ProfileA", "example.com", "console.com") + mock_internal = create_internal_object(True, "ProfileA") + setup_parser_one_profile(mock_profile, mock_internal, mock_config_parser) + accessor.set_username("TestUser", "ProfileA") + assert mock_saver.call_count + + def test_set_authority_marks_as_complete_if_ready(self, mock_config_parser): + mock_config_parser.sections.return_value = ["Internal", "ProfileA"] + accessor = ConfigAccessor(mock_config_parser) + mock_profile = create_mock_profile_object("ProfileA", None, "test.testerson") + mock_internal = create_internal_object(False) + setup_parser_one_profile(mock_profile, mock_internal, mock_config_parser) + accessor.set_authority_url("new url", "ProfileA") + assert mock_internal[ConfigAccessor.DEFAULT_PROFILE] == "ProfileA" + assert mock_internal[ConfigAccessor.DEFAULT_PROFILE_IS_COMPLETE] + + def test_set_authority_does_not_mark_as_complete_if_not_have_username(self, mock_config_parser): + mock_config_parser.sections.return_value = ["Internal", "ProfileA"] + accessor = ConfigAccessor(mock_config_parser) + mock_profile = create_mock_profile_object("ProfileA", None, None) + mock_internal = create_internal_object(False) + setup_parser_one_profile(mock_profile, mock_internal, mock_config_parser) + accessor.set_authority_url("new url", "ProfileA") + assert mock_internal[ConfigAccessor.DEFAULT_PROFILE] == ConfigAccessor.DEFAULT_VALUE + assert not mock_internal[ConfigAccessor.DEFAULT_PROFILE_IS_COMPLETE] + + def test_set_authority_saves(self, mock_config_parser, mock_saver): + mock_config_parser.sections.return_value = ["Internal", "ProfileA"] + accessor = ConfigAccessor(mock_config_parser) + mock_profile = create_mock_profile_object("ProfileA", None, None) + mock_internal = create_internal_object(True, "ProfileA") + setup_parser_one_profile(mock_profile, mock_internal, mock_config_parser) + accessor.set_authority_url("new url", "ProfileA") + assert mock_saver.call_count + + def test_switch_default_profile_switches_internal_value(self, mock_config_parser): + mock_config_parser.sections.return_value = ["Internal", "ProfileA", "ProfileB"] + accessor = ConfigAccessor(mock_config_parser) + mock_profile_a = create_mock_profile_object("ProfileA", "test", "test") + mock_profile_b = create_mock_profile_object("ProfileB", "test", "test") + + mock_internal = create_internal_object(True, "ProfileA") + + def side_effect(item): + if item == "ProfileA": + return mock_profile_a + elif item == "ProfileB": + return mock_profile_b + elif item == "Internal": + return mock_internal + + mock_config_parser.__getitem__.side_effect = side_effect + accessor.switch_default_profile("ProfileB") + assert mock_internal[ConfigAccessor.DEFAULT_PROFILE] == "ProfileB" + + def test_switch_default_profile_saves(self, mock_config_parser, mock_saver): + mock_config_parser.sections.return_value = ["Internal", "ProfileA", "ProfileB"] + accessor = ConfigAccessor(mock_config_parser) + mock_profile_a = create_mock_profile_object("ProfileA", "test", "test") + mock_profile_b = create_mock_profile_object("ProfileB", "test", "test") + + mock_internal = create_internal_object(True, "ProfileA") + + def side_effect(item): + if item == "ProfileA": + return mock_profile_a + elif item == "ProfileB": + return mock_profile_b + elif item == "Internal": + return mock_internal + + mock_config_parser.__getitem__.side_effect = side_effect + accessor.switch_default_profile("ProfileB") + assert mock_saver.call_count + + def test_create_profile_if_not_exists_when_given_default_name_does_not_create( + self, mock_config_parser + ): + mock_config_parser.sections.return_value = ["Internal", "ProfileA"] + accessor = ConfigAccessor(mock_config_parser) + with pytest.raises(Exception): + accessor.create_profile_if_not_exists(ConfigAccessor.DEFAULT_VALUE) diff --git a/tests/profile/test_password.py b/tests/profile/test_password.py index 00a53660c..2665f07fd 100644 --- a/tests/profile/test_password.py +++ b/tests/profile/test_password.py @@ -1,6 +1,24 @@ import pytest import code42cli.profile.password as password +from code42cli.profile.config import ConfigAccessor +from .conftest import PASSWORD_NAMESPACE + +_USERNAME = "test.username" + + +@pytest.fixture +def config_accessor(mocker): + mock = mocker.MagicMock(spec=ConfigAccessor) + factory = mocker.patch("{0}.get_config_accessor".format(PASSWORD_NAMESPACE)) + factory.return_value = mock + + class MockConfigProfile(object): + def __getitem__(self, item): + return _USERNAME + + mock.get_profile.return_value = MockConfigProfile() + return mock @pytest.fixture @@ -19,27 +37,26 @@ def getpass_function(mocker): def test_get_password_uses_expected_service_name_and_username( - keyring_password_getter, config_profile + keyring_password_getter, config_accessor ): - password.get_password() - expected_service_name = "code42cli::https://authority.example.com" - expected_username = "test.username" - keyring_password_getter.assert_called_once_with(expected_service_name, expected_username) + password.get_password("profile_name") + expected_service_name = "code42cli::profile_name" + keyring_password_getter.assert_called_once_with(expected_service_name, _USERNAME) def test_get_password_returns_expected_password( - keyring_password_getter, config_profile, keyring_password_setter + keyring_password_getter, config_accessor, keyring_password_setter ): keyring_password_getter.return_value = "already stored password 123" - assert password.get_password() == "already stored password 123" + assert password.get_password("profile_name") == "already stored password 123" def test_set_password_from_prompt_uses_expected_service_name_username_and_password( - keyring_password_setter, config_profile, getpass_function + keyring_password_setter, config_accessor, getpass_function ): getpass_function.return_value = "test password" - password.set_password_from_prompt() - expected_service_name = "code42cli::https://authority.example.com" + password.set_password_from_prompt("profile_name") + expected_service_name = "code42cli::profile_name" expected_username = "test.username" keyring_password_setter.assert_called_once_with( expected_service_name, expected_username, "test password" diff --git a/tests/profile/test_profile.py b/tests/profile/test_profile.py index d509529f3..563f9fe3d 100644 --- a/tests/profile/test_profile.py +++ b/tests/profile/test_profile.py @@ -3,27 +3,16 @@ import pytest from code42cli.profile import profile -from .conftest import CONFIG_NAMESPACE, PASSWORD_NAMESPACE, PROFILE_NAMESPACE +from code42cli.profile.config import ConfigAccessor +from .conftest import PASSWORD_NAMESPACE, PROFILE_NAMESPACE -@pytest.fixture(autouse=True) -def username_setter(mocker): - return mocker.patch("{0}.set_username".format(CONFIG_NAMESPACE)) - - -@pytest.fixture(autouse=True) -def mark_as_set_function(mocker): - return mocker.patch("{0}.mark_as_set_if_complete".format(CONFIG_NAMESPACE)) - - -@pytest.fixture(autouse=True) -def authority_url_setter(mocker): - return mocker.patch("{0}.set_authority_url".format(CONFIG_NAMESPACE)) - - -@pytest.fixture(autouse=True) -def ignore_ssl_errors_setter(mocker): - return mocker.patch("{0}.set_ignore_ssl_errors".format(CONFIG_NAMESPACE)) +@pytest.fixture +def config_accessor(mocker): + mock = mocker.MagicMock(spec=ConfigAccessor) + factory = mocker.patch("{0}.profile.get_config_accessor".format(PROFILE_NAMESPACE)) + factory.return_value = mock + return mock @pytest.fixture(autouse=True) @@ -47,34 +36,44 @@ def _get_profile_parser(): return subcommand_parser.choices.get("profile") +def create_profile(): + class MockSection(object): + name = "TEST" + + def get(*args): + pass + + return profile.Code42Profile(MockSection()) + + class TestCode42Profile(object): def test_get_password_when_is_none_returns_password_from_getpass(self, mocker, password_getter): password_getter.return_value = None mock_getpass = mocker.patch("{0}.get_password_from_prompt".format(PASSWORD_NAMESPACE)) mock_getpass.return_value = "Test Password" - actual = profile.Code42Profile().get_password() + actual = create_profile().get_password() assert actual == "Test Password" def test_get_password_return_password_from_password_get_password(self, password_getter): password_getter.return_value = "Test Password" - actual = profile.Code42Profile().get_password() + actual = create_profile().get_password() assert actual == "Test Password" -def test_init_adds_profile_subcommand_to_choices(config_parser): +def test_init_adds_profile_subcommand_to_choices(config_accessor): subcommand_parser = ArgumentParser().add_subparsers() profile.init(subcommand_parser) assert subcommand_parser.choices.get("profile") -def test_init_adds_parser_that_can_parse_show_command(config_parser): +def test_init_adds_parser_that_can_parse_show_command(config_accessor): subcommand_parser = ArgumentParser().add_subparsers() profile.init(subcommand_parser) profile_parser = subcommand_parser.choices.get("profile") - assert profile_parser.parse_args(["show"]) + assert profile_parser.parse_args(["show", "--profile", "name"]) -def test_init_adds_parser_that_can_parse_set_command(config_parser): +def test_init_adds_parser_that_can_parse_set_command(config_accessor): subcommand_parser = ArgumentParser().add_subparsers() profile.init(subcommand_parser) profile_parser = subcommand_parser.choices.get("profile") @@ -83,61 +82,83 @@ def test_init_adds_parser_that_can_parse_set_command(config_parser): ) -def test_get_profile_returns_object_from_config_profile(config_parser, config_profile): +def test_init_add_parser_that_can_parse_list_command(): + subcommand_parser = ArgumentParser().add_subparsers() + profile.init(subcommand_parser) + profile_parser = subcommand_parser.choices.get("profile") + assert profile_parser.parse_args(["list"]) + + +def test_init_add_parser_that_can_parse_use_command(): + subcommand_parser = ArgumentParser().add_subparsers() + profile.init(subcommand_parser) + profile_parser = subcommand_parser.choices.get("profile") + assert profile_parser.parse_args(["use", "name"]) + + +def test_get_profile_returns_object_from_config_profile(mocker, config_accessor): + expected = mocker.MagicMock() + config_accessor.get_profile.return_value = expected user = profile.get_profile() - # Values from config_profile fixture - assert ( - user.username == "test.username" - and user.authority_url == "https://authority.example.com" - and user.ignore_ssl_errors - ) + assert user._profile == expected -def test_set_profile_when_given_username_sets_username(config_parser, username_setter): +def test_set_profile_when_given_username_sets_username(config_accessor): parser = _get_profile_parser() namespace = parser.parse_args(["set", "-u", "a.new.user@example.com"]) profile.set_profile(namespace) - username_setter.assert_called_once_with("a.new.user@example.com") + assert config_accessor.set_username.call_args[0][0] == "a.new.user@example.com" -def test_set_profile_when_given_authority_url_sets_authority_url( - config_parser, authority_url_setter -): +def test_set_profile_when_given_profile_name_sets_username_for_profile(config_accessor): parser = _get_profile_parser() - namespace = parser.parse_args(["set", "-s", "https://wwww.new.authority.example.com"]) + namespace = parser.parse_args(["set", "--profile", "profileA", "-u", "a.new.user@example.com"]) profile.set_profile(namespace) - authority_url_setter.assert_called_once_with("https://wwww.new.authority.example.com") + assert config_accessor.set_username.call_args[0][0] == "a.new.user@example.com" + assert config_accessor.set_username.call_args[0][1] == "profileA" -def test_set_profile_when_given_enable_ssl_errors_sets_ignore_ssl_errors_to_true( - config_parser, ignore_ssl_errors_setter -): +def test_set_profile_when_given_authority_sets_authority(config_accessor): + parser = _get_profile_parser() + namespace = parser.parse_args(["set", "-s", "example.com"]) + profile.set_profile(namespace) + assert config_accessor.set_authority_url.call_args[0][0] == "example.com" + + +def test_set_profile_when_given_profile_name_sets_authority_for_profile(config_accessor): + parser = _get_profile_parser() + namespace = parser.parse_args(["set", "--profile", "profileA", "-s", "example.com"]) + profile.set_profile(namespace) + assert config_accessor.set_authority_url.call_args[0][0] == "example.com" + assert config_accessor.set_authority_url.call_args[0][1] == "profileA" + + +def test_set_profile_when_given_enable_ssl_errors_sets_ignore_ssl_errors_to_true(config_accessor): parser = _get_profile_parser() namespace = parser.parse_args(["set", "--enable-ssl-errors"]) profile.set_profile(namespace) - ignore_ssl_errors_setter.assert_called_once_with(False) + assert config_accessor.set_ignore_ssl_errors.call_args[0][0] == False -def test_set_profile_when_given_disable_ssl_errors_sets_ignore_ssl_errors_to_false( - config_parser, ignore_ssl_errors_setter -): +def test_set_profile_when_given_disable_ssl_errors_sets_ignore_ssl_errors_to_true(config_accessor): parser = _get_profile_parser() namespace = parser.parse_args(["set", "--disable-ssl-errors"]) profile.set_profile(namespace) - ignore_ssl_errors_setter.assert_called_once_with(True) + assert config_accessor.set_ignore_ssl_errors.call_args[0][0] == True -def test_set_profile_calls_marks_as_set_if_complete(config_parser, mark_as_set_function): +def test_set_profile_when_given_disable_ssl_errors_and_profile_name_sets_ignore_ssl_errors_to_true_for_profile( + config_accessor +): parser = _get_profile_parser() - namespace = parser.parse_args( - ["set", "-s", "https://wwww.new.authority.example.com", "-u", "user"] - ) + namespace = parser.parse_args(["set", "--profile", "profileA", "--disable-ssl-errors"]) profile.set_profile(namespace) - assert mark_as_set_function.call_count + assert config_accessor.set_ignore_ssl_errors.call_args[0][0] == True + assert config_accessor.set_ignore_ssl_errors.call_args[0][1] == "profileA" -def test_set_profile_when_told_to_store_password_prompts_for_storing_password( - mocker, input_function +def test_set_profile_when_to_store_password_prompts_for_storing_password( + mocker, config_accessor, input_function ): input_function.return_value = "y" mock_set_password_function = mocker.patch("code42cli.profile.password.set_password_from_prompt") @@ -150,7 +171,7 @@ def test_set_profile_when_told_to_store_password_prompts_for_storing_password( def test_set_profile_when_told_to_store_password_using_capital_y_prompts_for_storing_password( - mocker, input_function + mocker, config_accessor, input_function ): input_function.return_value = "Y" mock_set_password_function = mocker.patch("code42cli.profile.password.set_password_from_prompt") @@ -163,7 +184,7 @@ def test_set_profile_when_told_to_store_password_using_capital_y_prompts_for_sto def test_set_profile_when_told_not_to_store_password_prompts_for_storing_password( - mocker, input_function + mocker, config_accessor, input_function ): input_function.return_value = "n" mock_set_password_function = mocker.patch("code42cli.profile.password.set_password_from_prompt") @@ -175,7 +196,8 @@ def test_set_profile_when_told_not_to_store_password_prompts_for_storing_passwor assert not mock_set_password_function.call_count -def test_prompt_for_password_reset_calls_password_set_password_from_prompt(mocker): +def test_prompt_for_password_reset_calls_password_set_password_from_prompt(mocker, namespace): + namespace.profile_name = "profile name" mock_set_password_function = mocker.patch("code42cli.profile.password.set_password_from_prompt") - profile.prompt_for_password_reset() + profile.prompt_for_password_reset(namespace) assert mock_set_password_function.call_count diff --git a/tests/securitydata/conftest.py b/tests/securitydata/conftest.py index 265404562..28b64ff0d 100644 --- a/tests/securitydata/conftest.py +++ b/tests/securitydata/conftest.py @@ -1,3 +1,5 @@ +import pytest + from tests.conftest import get_test_date_str SECURITYDATA_NAMESPACE = "code42cli.securitydata" @@ -8,3 +10,8 @@ begin_date_tuple_with_time = (get_test_date_str(days_ago=89), "3:12:33") end_date_tuple = (get_test_date_str(days_ago=10),) end_date_tuple_with_time = (get_test_date_str(days_ago=10), "11:22:43") + + +@pytest.fixture(autouse=True) +def sqlite_connection(mocker): + return mocker.patch("sqlite3.connect") diff --git a/tests/securitydata/subcommands/test_clear_checkpoint.py b/tests/securitydata/subcommands/test_clear_checkpoint.py index 7dc664a78..5f5717cda 100644 --- a/tests/securitydata/subcommands/test_clear_checkpoint.py +++ b/tests/securitydata/subcommands/test_clear_checkpoint.py @@ -3,19 +3,41 @@ from code42cli.securitydata.subcommands import clear_checkpoint as clearer from ..conftest import SECURITYDATA_NAMESPACE -_CURSOR_STORE_PATH = "{0}.cursor_store".format(SECURITYDATA_NAMESPACE) +_CURSOR_STORE_NAMESPACE = "{0}.cursor_store".format(SECURITYDATA_NAMESPACE) @pytest.fixture def cursor_store(mocker): - mock_init = mocker.patch("{0}.FileEventCursorStore.__init__".format(_CURSOR_STORE_PATH)) + mock_init = mocker.patch("{0}.FileEventCursorStore.__init__".format(_CURSOR_STORE_NAMESPACE)) mock_init.return_value = None mock = mocker.MagicMock() - mock_new = mocker.patch("{0}.FileEventCursorStore.__new__".format(_CURSOR_STORE_PATH)) + mock_new = mocker.patch("{0}.FileEventCursorStore.__new__".format(_CURSOR_STORE_NAMESPACE)) mock_new.return_value = mock return mock -def test_clear_checkpoint_calls_cursor_store_reset(cursor_store): - clearer.clear_checkpoint() - assert cursor_store.reset.call_count == 1 +@pytest.fixture +def profile(mocker): + class MockProfile(object): + @property + def name(self): + return "AlreadySetProfileName" + + mock = mocker.patch( + "{0}.subcommands.clear_checkpoint.get_profile".format(SECURITYDATA_NAMESPACE) + ) + mock.return_value = MockProfile() + return mock + + +def test_clear_checkpoint_when_given_profile_name_calls_cursor_store_resets( + cursor_store, namespace +): + namespace.profile_name = "Test" + clearer.clear_checkpoint(namespace) + assert cursor_store.replace_stored_insertion_timestamp.call_args[0][0] is None + + +def test_clear_checkpoint_calls_cursor_store_resets(cursor_store, namespace, profile): + clearer.clear_checkpoint(namespace) + assert cursor_store.replace_stored_insertion_timestamp.call_args[0][0] is None diff --git a/tests/securitydata/subcommands/test_print_out.py b/tests/securitydata/subcommands/test_print_out.py index 60b632140..d99ce361b 100644 --- a/tests/securitydata/subcommands/test_print_out.py +++ b/tests/securitydata/subcommands/test_print_out.py @@ -3,12 +3,21 @@ import pytest import code42cli.securitydata.subcommands.print_out as printer +from code42cli.profile.config import ConfigAccessor from .conftest import ACCEPTABLE_ARGS from ..conftest import SUBCOMMANDS_NAMESPACE _PRINT_PATH = "{0}.print_out".format(SUBCOMMANDS_NAMESPACE) +@pytest.fixture +def config_accessor(mocker): + mock = mocker.MagicMock(spec=ConfigAccessor) + factory = mocker.patch("") + factory.return_value = mock + return mock + + @pytest.fixture def logger_factory(mocker): return mocker.patch("{0}.get_logger_for_stdout".format(_PRINT_PATH)) @@ -19,7 +28,7 @@ def extractor(mocker): return mocker.patch("{0}.extract".format(_PRINT_PATH)) -def test_init_adds_parser_that_can_parse_supported_args(config_parser): +def test_init_adds_parser_that_can_parse_supported_args(): subcommand_parser = ArgumentParser().add_subparsers() printer.init(subcommand_parser) print_parser = subcommand_parser.choices.get("print") diff --git a/tests/securitydata/subcommands/test_send_to.py b/tests/securitydata/subcommands/test_send_to.py index 27df08274..0ccea50c0 100644 --- a/tests/securitydata/subcommands/test_send_to.py +++ b/tests/securitydata/subcommands/test_send_to.py @@ -27,7 +27,7 @@ def extractor(mocker): return mocker.patch("{0}.extract".format(_SEND_PATH)) -def test_init_adds_parser_that_can_parse_supported_args(config_parser): +def test_init_adds_parser_that_can_parse_supported_args(): subcommand_parser = ArgumentParser().add_subparsers() sender.init(subcommand_parser) send_parser = subcommand_parser.choices.get("send-to") @@ -35,7 +35,7 @@ def test_init_adds_parser_that_can_parse_supported_args(config_parser): send_parser.parse_args(args) -def test_init_adds_parser_when_not_given_server_causes_system_exit(config_parser): +def test_init_adds_parser_when_not_given_server_causes_system_exit(): subcommand_parser = ArgumentParser().add_subparsers() sender.init(subcommand_parser) send_parser = subcommand_parser.choices.get("send-to") diff --git a/tests/securitydata/subcommands/test_write_to.py b/tests/securitydata/subcommands/test_write_to.py index 379846751..1abbe4771 100644 --- a/tests/securitydata/subcommands/test_write_to.py +++ b/tests/securitydata/subcommands/test_write_to.py @@ -26,7 +26,7 @@ def extractor(mocker): return mocker.patch("{0}.extract".format(_WRITE_PATH)) -def test_init_adds_parser_that_can_parse_supported_args(config_parser): +def test_init_adds_parser_that_can_parse_supported_args(): subcommand_parser = ArgumentParser().add_subparsers() writer.init(subcommand_parser) write_parser = subcommand_parser.choices.get("write-to") @@ -34,7 +34,7 @@ def test_init_adds_parser_that_can_parse_supported_args(config_parser): write_parser.parse_args(args) -def test_init_adds_parser_when_not_given_filename_causes_system_exit(config_parser): +def test_init_adds_parser_when_not_given_filename_causes_system_exit(): subcommand_parser = ArgumentParser().add_subparsers() writer.init(subcommand_parser) write_parser = subcommand_parser.choices.get("write-to") diff --git a/tests/securitydata/test_cursor_store.py b/tests/securitydata/test_cursor_store.py index 3d40161b9..37f2732a4 100644 --- a/tests/securitydata/test_cursor_store.py +++ b/tests/securitydata/test_cursor_store.py @@ -1,16 +1,10 @@ from os import path -import pytest from c42eventextractor.extractors import INSERTION_TIMESTAMP_FIELD_NAME from code42cli.securitydata.cursor_store import BaseCursorStore, FileEventCursorStore -@pytest.fixture -def sqlite_connection(mocker): - return mocker.patch("sqlite3.connect") - - class TestBaseCursorStore(object): def test_init_cursor_store_when_not_given_db_file_path_uses_expected_path_with_db_table_name_as_db_file_name( self, sqlite_connection @@ -29,47 +23,23 @@ def test_init_cursor_store_when_given_db_file_path_uses_given_path(self, sqlite_ class TestFileEventCursorStore(object): - MOCK_TEST_DB_PATH = "test_path.db" - - def test_reset_executes_expected_drop_table_query(self, sqlite_connection): - store = FileEventCursorStore(self.MOCK_TEST_DB_PATH) - store.reset() - with store._connection as conn: - actual = conn.execute.call_args_list[0][0][0] - expected = "DROP TABLE aed_checkpoint" - assert actual == expected - - def test_reset_executes_expected_create_table_query(self, sqlite_connection): - store = FileEventCursorStore(self.MOCK_TEST_DB_PATH) - store.reset() - with store._connection as conn: - actual = conn.execute.call_args_list[1][0][0] - expected = "CREATE TABLE aed_checkpoint (cursor_id, insertionTimestamp)" - assert actual == expected + MOCK_TEST_DB_NAME = "test_path.db" - def test_reset_executes_expected_insert_query(self, sqlite_connection): - store = FileEventCursorStore(self.MOCK_TEST_DB_PATH) - store._connection = sqlite_connection - store.reset() - with store._connection as conn: - actual = conn.execute.call_args[0][0] - expected = "INSERT INTO aed_checkpoint VALUES(?, null)" - assert actual == expected - - def test_reset_executes_query_with_expected_primary_key(self, sqlite_connection): - store = FileEventCursorStore(self.MOCK_TEST_DB_PATH) - store._connection = sqlite_connection - store.reset() - with store._connection as conn: - actual = conn.execute.call_args[0][1][0] - expected = store._PRIMARY_KEY - assert actual == expected + def test_init_when_called_twice_with_different_profile_names_creates_two_rows( + self, mocker, sqlite_connection + ): + mock = mocker.patch("code42cli.securitydata.cursor_store.FileEventCursorStore._row_exists") + mock.return_value = False + spy = mocker.spy(FileEventCursorStore, "_insert_new_row") + FileEventCursorStore("Profile A", self.MOCK_TEST_DB_NAME) + FileEventCursorStore("Profile B", self.MOCK_TEST_DB_NAME) + assert spy.call_count == 2 def test_get_stored_insertion_timestamp_executes_expected_select_query(self, sqlite_connection): - store = FileEventCursorStore(self.MOCK_TEST_DB_PATH) + store = FileEventCursorStore("Profile", self.MOCK_TEST_DB_NAME) store.get_stored_insertion_timestamp() with store._connection as conn: - expected = "SELECT {0} FROM aed_checkpoint WHERE cursor_id=?".format( + expected = "SELECT {0} FROM file_event_checkpoints WHERE cursor_id=?".format( INSERTION_TIMESTAMP_FIELD_NAME ) actual = conn.cursor().execute.call_args[0][0] @@ -78,20 +48,20 @@ def test_get_stored_insertion_timestamp_executes_expected_select_query(self, sql def test_get_stored_insertion_timestamp_executes_query_with_expected_primary_key( self, sqlite_connection ): - store = FileEventCursorStore(self.MOCK_TEST_DB_PATH) + store = FileEventCursorStore("Profile", self.MOCK_TEST_DB_NAME) store.get_stored_insertion_timestamp() with store._connection as conn: actual = conn.cursor().execute.call_args[0][1][0] - expected = store._PRIMARY_KEY + expected = store._primary_key assert actual == expected def test_replace_stored_insertion_timestamp_executes_expected_update_query( self, sqlite_connection ): - store = FileEventCursorStore(self.MOCK_TEST_DB_PATH) + store = FileEventCursorStore("Profile", self.MOCK_TEST_DB_NAME) store.replace_stored_insertion_timestamp(123) with store._connection as conn: - expected = "UPDATE aed_checkpoint SET {0}=? WHERE cursor_id=?".format( + expected = "UPDATE file_event_checkpoints SET {0}=? WHERE cursor_id=?".format( INSERTION_TIMESTAMP_FIELD_NAME ) actual = conn.execute.call_args[0][0] @@ -100,7 +70,7 @@ def test_replace_stored_insertion_timestamp_executes_expected_update_query( def test_replace_stored_insertion_timestamp_executes_query_with_expected_primary_key( self, sqlite_connection ): - store = FileEventCursorStore(self.MOCK_TEST_DB_PATH) + store = FileEventCursorStore("Profile", self.MOCK_TEST_DB_NAME) new_insertion_timestamp = 123 store.replace_stored_insertion_timestamp(new_insertion_timestamp) with store._connection as conn: diff --git a/tests/securitydata/test_date_helper.py b/tests/securitydata/test_date_helper.py index 4dab3a502..f57e8b44c 100644 --- a/tests/securitydata/test_date_helper.py +++ b/tests/securitydata/test_date_helper.py @@ -61,11 +61,6 @@ def test_create_event_timestamp_filter_when_end_is_before_begin_causes_value_err create_event_timestamp_filter(begin_date_tuple, end_date_str) -def test_create_event_timestamp_filter_when_given_minutes_ago_and_time_raises_value_error(): - with pytest.raises(ValueError): - create_event_timestamp_filter("600", "12:00:00") - - def test_create_event_timestamp_filter_when_given_three_date_args_raises_value_error(): begin_date_tuple = (get_test_date_str(days_ago=5), "12:00:00", "end_date=12:00:00") with pytest.raises(ValueError): diff --git a/tests/securitydata/test_extraction.py b/tests/securitydata/test_extraction.py index b71deb03f..504b6db2b 100644 --- a/tests/securitydata/test_extraction.py +++ b/tests/securitydata/test_extraction.py @@ -89,63 +89,63 @@ def test_extract_when_is_advanced_query_and_has_exposure_types_exits(logger, nam def test_extract_when_is_advanced_query_and_has_username_exists(logger, namespace): namespace.advanced_query = "some complex json" - namespace.c42username = "Someone" + namespace.c42usernames = ["Someone"] with pytest.raises(SystemExit): extraction_module.extract(logger, namespace) def test_extract_when_is_advanced_query_and_has_actor_exists(logger, namespace): namespace.advanced_query = "some complex json" - namespace.actor = "Someone" + namespace.actors = ["Someone"] with pytest.raises(SystemExit): extraction_module.extract(logger, namespace) def test_extract_when_is_advanced_query_and_has_md5_exists(logger, namespace): namespace.advanced_query = "some complex json" - namespace.md5 = "098f6bcd4621d373cade4e832627b4f6" + namespace.md5_hashes = ["098f6bcd4621d373cade4e832627b4f6"] with pytest.raises(SystemExit): extraction_module.extract(logger, namespace) def test_extract_when_is_advanced_query_and_has_sha256_exists(logger, namespace): namespace.advanced_query = "some complex json" - namespace.sha256 = "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" + namespace.sha256_hashes = ["9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"] with pytest.raises(SystemExit): extraction_module.extract(logger, namespace) def test_extract_when_is_advanced_query_and_has_source_exists(logger, namespace): namespace.advanced_query = "some complex json" - namespace.source = "Gmail" + namespace.sources = ["Gmail"] with pytest.raises(SystemExit): extraction_module.extract(logger, namespace) def test_extract_when_is_advanced_query_and_has_filename_exists(logger, namespace): namespace.advanced_query = "some complex json" - namespace.filename = "test.out" + namespace.filenames = ["test.out"] with pytest.raises(SystemExit): extraction_module.extract(logger, namespace) def test_extract_when_is_advanced_query_and_has_filepath_exists(logger, namespace): namespace.advanced_query = "some complex json" - namespace.filepath = "path/to/file" + namespace.filepaths = ["path/to/file"] with pytest.raises(SystemExit): extraction_module.extract(logger, namespace) def test_extract_when_is_advanced_query_and_has_process_owner_exists(logger, namespace): namespace.advanced_query = "some complex json" - namespace.process_owner = "someone" + namespace.process_owners = ["someone"] with pytest.raises(SystemExit): extraction_module.extract(logger, namespace) def test_extract_when_is_advanced_query_and_has_tab_url_exists(logger, namespace): namespace.advanced_query = "some complex json" - namespace.tab_url = "https://www.example.com" + namespace.tab_urls = ["https://www.example.com"] with pytest.raises(SystemExit): extraction_module.extract(logger, namespace) @@ -164,6 +164,14 @@ def test_extract_when_is_advanced_query_and_has_include_non_exposure_exits(logge extraction_module.extract(logger, namespace) +def test_extract_when_is_advanced_query_and_include_non_exposure_is_false_does_not_exit( + logger, namespace +): + namespace.include_non_exposure_events = False + namespace.advanced_query = "some complex json" + extraction_module.extract(logger, namespace) + + def test_extract_when_is_advanced_query_and_has_incremental_mode_set_to_false_does_not_exit( logger, namespace ): @@ -327,65 +335,75 @@ def test_extract_when_given_end_date_with_len_3_causes_exit( def test_extract_when_given_username_uses_username_filter(logger, namespace_with_begin, extractor): - namespace_with_begin.c42username = "test.testerson@example.com" + namespace_with_begin.c42usernames = ["test.testerson@example.com"] extraction_module.extract(logger, namespace_with_begin) assert str(extractor.extract.call_args[0][1]) == str( - DeviceUsername.eq(namespace_with_begin.c42username) + DeviceUsername.is_in(namespace_with_begin.c42usernames) ) def test_extract_when_given_actor_uses_actor_filter(logger, namespace_with_begin, extractor): - namespace_with_begin.actor = "test.testerson" + namespace_with_begin.actors = ["test.testerson"] extraction_module.extract(logger, namespace_with_begin) - assert str(extractor.extract.call_args[0][1]) == str(Actor.eq(namespace_with_begin.actor)) + assert str(extractor.extract.call_args[0][1]) == str(Actor.is_in(namespace_with_begin.actors)) def test_extract_when_given_md5_uses_md5_filter(logger, namespace_with_begin, extractor): - namespace_with_begin.md5 = "098f6bcd4621d373cade4e832627b4f6" + namespace_with_begin.md5_hashes = ["098f6bcd4621d373cade4e832627b4f6"] extraction_module.extract(logger, namespace_with_begin) - assert str(extractor.extract.call_args[0][1]) == str(MD5.eq(namespace_with_begin.md5)) + assert str(extractor.extract.call_args[0][1]) == str(MD5.is_in(namespace_with_begin.md5_hashes)) def test_extract_when_given_sha256_uses_sha256_filter(logger, namespace_with_begin, extractor): - namespace_with_begin.sha256 = "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" + namespace_with_begin.sha256_hashes = [ + "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" + ] extraction_module.extract(logger, namespace_with_begin) - assert str(extractor.extract.call_args[0][1]) == str(SHA256.eq(namespace_with_begin.sha256)) + assert str(extractor.extract.call_args[0][1]) == str( + SHA256.is_in(namespace_with_begin.sha256_hashes) + ) def test_extract_when_given_source_uses_source_filter(logger, namespace_with_begin, extractor): - namespace_with_begin.source = "Gmail" + namespace_with_begin.sources = ["Gmail", "Yahoo"] extraction_module.extract(logger, namespace_with_begin) - assert str(extractor.extract.call_args[0][1]) == str(Source.eq(namespace_with_begin.source)) + assert str(extractor.extract.call_args[0][1]) == str(Source.is_in(namespace_with_begin.sources)) def test_extract_when_given_filename_uses_filename_filter(logger, namespace_with_begin, extractor): - namespace_with_begin.filename = "file.txt" + namespace_with_begin.filenames = ["file.txt", "txt.file"] extraction_module.extract(logger, namespace_with_begin) - assert str(extractor.extract.call_args[0][1]) == str(FileName.eq(namespace_with_begin.filename)) + assert str(extractor.extract.call_args[0][1]) == str( + FileName.is_in(namespace_with_begin.filenames) + ) def test_extract_when_given_filepath_uses_filepath_filter(logger, namespace_with_begin, extractor): - namespace_with_begin.filepath = "/path/to/file.txt" + namespace_with_begin.filepaths = ["/path/to/file.txt", "path2"] extraction_module.extract(logger, namespace_with_begin) - assert str(extractor.extract.call_args[0][1]) == str(FilePath.eq(namespace_with_begin.filepath)) + assert str(extractor.extract.call_args[0][1]) == str( + FilePath.is_in(namespace_with_begin.filepaths) + ) def test_extract_when_given_process_owner_uses_process_owner_filter( logger, namespace_with_begin, extractor ): - namespace_with_begin.process_owner = "test.testerson" + namespace_with_begin.process_owners = ["test.testerson", "another"] extraction_module.extract(logger, namespace_with_begin) assert str(extractor.extract.call_args[0][1]) == str( - ProcessOwner.eq(namespace_with_begin.process_owner) + ProcessOwner.is_in(namespace_with_begin.process_owners) ) def test_extract_when_given_tab_url_uses_process_tab_url_filter( logger, namespace_with_begin, extractor ): - namespace_with_begin.tab_url = "https://www.example.com" + namespace_with_begin.tab_urls = ["https://www.example.com"] extraction_module.extract(logger, namespace_with_begin) - assert str(extractor.extract.call_args[0][1]) == str(TabURL.eq(namespace_with_begin.tab_url)) + assert str(extractor.extract.call_args[0][1]) == str( + TabURL.is_in(namespace_with_begin.tab_urls) + ) def test_extract_when_given_exposure_types_uses_exposure_type_is_in_filter( @@ -418,13 +436,19 @@ def test_extract_when_not_given_include_non_exposure_includes_exposure_type_exis def test_extract_when_given_multiple_search_args_uses_expected_filters( logger, namespace_with_begin, extractor ): - namespace_with_begin.filepath = "/path/to/file.txt" - namespace_with_begin.process_owner = "test.testerson" - namespace_with_begin.tab_url = "https://www.example.com" + namespace_with_begin.filepaths = ["/path/to/file.txt"] + namespace_with_begin.process_owners = ["test.testerson", "flag.flagerson"] + namespace_with_begin.tab_urls = ["https://www.example.com"] extraction_module.extract(logger, namespace_with_begin) - assert str(extractor.extract.call_args[0][1]) == str(FilePath.eq("/path/to/file.txt")) - assert str(extractor.extract.call_args[0][2]) == str(ProcessOwner.eq("test.testerson")) - assert str(extractor.extract.call_args[0][3]) == str(TabURL.eq("https://www.example.com")) + assert str(extractor.extract.call_args[0][1]) == str( + FilePath.is_in(namespace_with_begin.filepaths) + ) + assert str(extractor.extract.call_args[0][2]) == str( + ProcessOwner.is_in(namespace_with_begin.process_owners) + ) + assert str(extractor.extract.call_args[0][3]) == str( + TabURL.is_in(namespace_with_begin.tab_urls) + ) def test_extract_when_given_include_non_exposure_and_exposure_types_causes_exit(