def test_happy_get_eyaml_values(self, quiet_logger, eyamldata_f, old_eyaml_keys, yaml_path, compare): processor = EYAMLProcessor(quiet_logger, eyamldata_f, privatekey=old_eyaml_keys[0], publickey=old_eyaml_keys[1]) for node in processor.get_eyaml_values(yaml_path, True): assert unwrap_node_coords(node) == compare
def __init__(self, config: DifferConfig, logger: ConsolePrinter, document: Any, **kwargs) -> None: """ Instantiate this class into an object. Parameters: 1. logger (ConsolePrinter) Instance of ConsoleWriter or subclass 2. document (Any) The basis document Keyword Arguments: * ignore_eyaml_values (bool) Do not decrypt encrypted YAML value for comparison Returns: N/A Raises: N/A """ ignore_eyaml = kwargs.pop("ignore_eyaml_values", True) self.config: DifferConfig = config self.logger: ConsolePrinter = logger self._data: Any = document self._diffs: List[DiffEntry] = [] self._ignore_eyaml: bool = ignore_eyaml self._eyamlproc = (None if ignore_eyaml else EYAMLProcessor( logger, document, **kwargs))
def main(): """Main code.""" args = processcli() log = ConsolePrinter(args) validateargs(args, log) yaml_path = YAMLPath(args.query, pathsep=args.pathsep) # Prep the YAML parser yaml = Parsers.get_yaml_editor() # Attempt to open the YAML file; check for parsing errors (yaml_data, doc_loaded) = Parsers.get_yaml_data( yaml, log, args.yaml_file if args.yaml_file else "-") if not doc_loaded: # An error message has already been logged sys.exit(1) # Seek the queried value(s) discovered_nodes = [] processor = EYAMLProcessor(log, yaml_data, binary=args.eyaml, publickey=args.publickey, privatekey=args.privatekey) try: for node in processor.get_eyaml_values(yaml_path, mustexist=True): log.debug("Got node from {}:".format(yaml_path), data=node, prefix="yaml_get::main: ") discovered_nodes.append(NodeCoords.unwrap_node_coords(node)) except YAMLPathException as ex: log.critical(ex, 1) except EYAMLCommandException as ex: log.critical(ex, 2) try: for node in discovered_nodes: if isinstance(node, (dict, list, CommentedSet)): print(json.dumps(Parsers.jsonify_yaml_data(node))) else: if node is None: node = "\x00" print("{}".format(str(node).replace("\n", r"\n"))) except RecursionError: log.critical( "The YAML data contains an infinitely recursing YAML Alias!", 1)
def test_find_eyaml_paths(self, quiet_logger, eyamldata_f): processor = EYAMLProcessor(quiet_logger, eyamldata_f) expected = [ "aliases[&secretIdentity]", "aliases[&secretPhrase]", "anchored::secrets.aliased_values.ident", "anchored::secrets.aliased_values.phrase", "anchored::secrets.array_of_array_idents[0][0]", "aliased::secrets.novel_values.ident", "aliased::secrets.novel_values.phrase", "aliased::secrets.string_values.ident", "aliased::secrets.string_values.phrase", ] actual = [] for path in processor.find_eyaml_paths(): actual.append(str(path)) assert actual == expected
def print_results(args: Any, processor: EYAMLProcessor, yaml_file: str, yaml_paths: List[Tuple[str, YAMLPath]], document_index: int) -> None: """Dump search results to STDOUT with optional and dynamic formatting.""" in_expressions = len(args.search) print_file_path = not args.nofile print_expression = in_expressions > 1 and not args.noexpression print_yaml_path = not args.noyamlpath print_value = args.values buffers = [ ": " if print_file_path or print_expression and (print_yaml_path or print_value) else "", ": " if print_yaml_path and print_value else "", ] for entry in yaml_paths: expression, result = entry resline = "" if print_file_path: display_file_name = ("STDIN" if yaml_file.strip() == "-" else yaml_file) resline += "{}/{}".format(display_file_name, document_index) if print_expression: resline += "[{}]".format(expression) resline += buffers[0] if print_yaml_path: if args.noescape: use_flash = args.pathsep is PathSeperators.FSLASH seglines = [] join_mark = "/" if use_flash else "." path_prefix = "/" if use_flash else "" for (_, segment) in result.escaped: seglines.append(str(segment)) resline += "{}{}".format(path_prefix, join_mark.join(seglines)) else: resline += "{}".format(result) resline += buffers[1] if print_value: # These results can have only one match, but make sure lest the # output become messy. for node_coordinate in processor.get_nodes(result, mustexist=True): node = node_coordinate.node if isinstance(node, (dict, list, CommentedSet)): resline += "{}".format( json.dumps(Parsers.jsonify_yaml_data(node))) else: resline += "{}".format(str(node).replace("\n", r"\n")) break print(resline)
def test_bad_encryption_keys(self, quiet_logger): processor = EYAMLProcessor(quiet_logger, None) processor.privatekey = "/no/such/file" processor.publickey = "/no/such/file" with pytest.raises(EYAMLCommandException): processor.encrypt_eyaml("test")
def new_eyaml_keys(tmp_path_factory): """Creates temporary keys for encryption/decryption tests.""" new_key_path_name = "new-keys" new_key_dir = tmp_path_factory.mktemp(new_key_path_name) new_private_key_file = new_key_dir / EYAML_PRIVATE_KEY_FILENAME new_public_key_file = new_key_dir / EYAML_PUBLIC_KEY_FILENAME run("{} createkeys --pkcs7-private-key={} --pkcs7-public-key={}".format( EYAMLProcessor.get_eyaml_executable("eyaml"), new_private_key_file, new_public_key_file).split(), check=True) return (new_private_key_file, new_public_key_file)
def test_preserve_old_blockiness(self, quiet_logger, eyamldata_f, old_eyaml_keys, yaml_path, newval, eoformat, yvformat): processor = EYAMLProcessor(quiet_logger, eyamldata_f, privatekey=old_eyaml_keys[0], publickey=old_eyaml_keys[1]) processor.set_eyaml_value(yaml_path, newval, output=eoformat) encvalue = None encformat = YAMLValueFormats.DEFAULT for encnode in processor.get_nodes(yaml_path): encvalue = encnode encformat = YAMLValueFormats.from_node(encvalue) break assert EYAMLProcessor.is_eyaml_value(encvalue) and yvformat == encformat
def test_happy_set_eyaml_value(self, quiet_logger, eyamldata_f, old_eyaml_keys, yaml_path, compare, mustexist, output_format): processor = EYAMLProcessor(quiet_logger, eyamldata_f, privatekey=old_eyaml_keys[0], publickey=old_eyaml_keys[1]) # Set the test value processor.set_eyaml_value(yaml_path, compare, output_format, mustexist) # Ensure the new value is encrypted encvalue = None for encnode in processor.get_nodes(yaml_path): encvalue = encnode break assert EYAMLProcessor.is_eyaml_value(encvalue)
def print_results(args: Any, processor: EYAMLProcessor, yaml_file: str, yaml_paths: List[Tuple[str, YAMLPath]]) -> None: """ Dumps the search results to STDOUT with optional and dynamic formatting. """ in_file_count = len(args.yaml_files) in_expressions = len(args.search) print_file_path = in_file_count > 1 and not args.nofile print_expression = in_expressions > 1 and not args.noexpression print_yaml_path = not args.noyamlpath print_value = args.values buffers = [ ": " if print_file_path or print_expression and (print_yaml_path or print_value) else "", ": " if print_yaml_path and print_value else "", ] for entry in yaml_paths: expression, result = entry resline = "" if print_file_path: resline += "{}".format(yaml_file) if print_expression: resline += "[{}]".format(expression) resline += buffers[0] if print_yaml_path: resline += "{}".format(result) resline += buffers[1] if print_value: # These results can have only one match, but make sure lest the # output become messy. for node in processor.get_nodes(result, mustexist=True): if isinstance(node, (dict, list)): resline += "{}".format(json.dumps(node)) else: resline += "{}".format(str(node).replace("\n", r"\n")) break print(resline)
def main(): """Main code.""" # Process any command-line arguments args = processcli() log = ConsolePrinter(args) validateargs(args, log) processor = EYAMLProcessor(log, None, binary=args.eyaml) # Prep the YAML parser yaml = Parsers.get_yaml_editor() # Process the input file(s) in_file_count = len(args.yaml_files) exit_state = 0 for yaml_file in args.yaml_files: file_changed = False backup_file = yaml_file + ".bak" seen_anchors = [] # Each YAML_FILE must actually be a file if not isfile(yaml_file): log.error("Not a file: {}".format(yaml_file)) exit_state = 2 continue # Don't bother with the file change update when there's only one input # file. if in_file_count > 1: log.info("Processing {}...".format(yaml_file)) # Try to open the file (yaml_data, doc_loaded) = Parsers.get_yaml_data(yaml, log, yaml_file) if not doc_loaded: # An error message has already been logged exit_state = 3 continue # Process all EYAML values processor.data = yaml_data for yaml_path in processor.find_eyaml_paths(): # Use ::get_nodes() instead of ::get_eyaml_values() here in order # to ignore values that have already been rotated via their # Anchors. for node_coordinate in processor.get_nodes(yaml_path, mustexist=True): # Ignore values which are Aliases for those already decrypted node = node_coordinate.node anchor_name = Anchors.get_node_anchor(node) if anchor_name is not None: if anchor_name in seen_anchors: continue seen_anchors.append(anchor_name) log.verbose("Decrypting value(s) at {}.".format(yaml_path)) processor.publickey = args.oldpublickey processor.privatekey = args.oldprivatekey try: txtval = processor.decrypt_eyaml(node) except EYAMLCommandException as ex: log.error(ex) exit_state = 3 continue # Prefer block (folded) values unless the original YAML value # was already a massivly long (string) line. output = EYAMLOutputFormats.BLOCK if not isinstance(node, FoldedScalarString): output = EYAMLOutputFormats.STRING # Re-encrypt the value with new EYAML keys processor.publickey = args.newpublickey processor.privatekey = args.newprivatekey try: processor.set_eyaml_value(yaml_path, txtval, output=output) except EYAMLCommandException as ex: log.error(ex) exit_state = 3 continue file_changed = True # Save the changes if file_changed: if args.backup: log.verbose("Saving a backup of {} to {}.".format( yaml_file, backup_file)) if exists(backup_file): remove(backup_file) copy2(yaml_file, backup_file) log.verbose("Writing changed data to {}.".format(yaml_file)) with open(yaml_file, 'w', encoding='utf-8') as yaml_dump: yaml.dump(yaml_data, yaml_dump) sys.exit(exit_state)
def test_none_eyaml_value(self): assert False == EYAMLProcessor.is_eyaml_value(None)
def test_decrypt_calledprocesserror(self, quiet_logger, force_subprocess_run_cpe): processor = EYAMLProcessor(quiet_logger, None) with pytest.raises(EYAMLCommandException): processor.decrypt_eyaml("ENC[...]")
def test_non_executable(self, old_eyaml_keys, force_no_access): assert EYAMLProcessor.get_eyaml_executable(str(old_eyaml_keys[0])) is None
def test_ignore_already_decrypted_cryps(self, quiet_logger): processor = EYAMLProcessor(quiet_logger, None) testval = "some value" assert testval == processor.decrypt_eyaml(testval)
def test_impossible_decryption(self, quiet_logger, old_eyaml_keys): processor = EYAMLProcessor(quiet_logger, None) testval = "ENC[...]" with pytest.raises(EYAMLCommandException): processor.decrypt_eyaml(testval)
def test_no_decrypt_without_eyaml(self, quiet_logger): processor = EYAMLProcessor(quiet_logger, None) processor.eyaml = None with pytest.raises(EYAMLCommandException): processor.decrypt_eyaml("ENC[...]")
def test_ignore_already_encrypted_cryps(self, quiet_logger): processor = EYAMLProcessor(quiet_logger, None) testval = "ENC[...]" assert testval == processor.encrypt_eyaml(testval)
import tempfile from subprocess import run from types import SimpleNamespace import pytest from yamlpath.wrappers import ConsolePrinter from yamlpath.eyaml import EYAMLProcessor # Implied constants EYAML_PRIVATE_KEY_FILENAME = "private_key.pkcs7.pem" EYAML_PUBLIC_KEY_FILENAME = "public_key.pkcs7.pem" # pylint: disable=locally-disabled,invalid-name requireseyaml = pytest.mark.skipif( EYAMLProcessor.get_eyaml_executable("eyaml") is None, reason="The 'eyaml' command must be installed and accessible on the PATH" + " to test and use EYAML features. Try: 'gem install hiera-eyaml'" + " after intalling ruby and rubygems.") @pytest.fixture def quiet_logger(): """Returns a quiet ConsolePrinter.""" args = SimpleNamespace(verbose=False, quiet=True, debug=False) return ConsolePrinter(args) @pytest.fixture def info_warn_logger(): """Returns a quiet ConsolePrinter."""
def main(): """Main code.""" args = processcli() log = ConsolePrinter(args) validateargs(args, log) change_path = YAMLPath(args.change, pathsep=args.pathsep) backup_file = args.yaml_file + ".bak" # Obtain the replacement value if args.value: new_value = args.value elif args.stdin: new_value = ''.join(sys.stdin.readlines()) elif args.file: with open(args.file, 'r') as fhnd: new_value = fhnd.read().rstrip() elif args.random is not None: new_value = ''.join( secrets.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(args.random)) # Prep the YAML parser yaml = get_yaml_editor() # Attempt to open the YAML file; check for parsing errors yaml_data = get_yaml_data(yaml, log, args.yaml_file) if yaml_data is None: # An error message has already been logged exit(1) # Load the present value at the specified YAML Path change_nodes = [] old_format = YAMLValueFormats.DEFAULT processor = EYAMLProcessor(log, yaml_data, binary=args.eyaml, publickey=args.publickey, privatekey=args.privatekey) try: for node in processor.get_nodes( change_path, mustexist=(args.mustexist or args.saveto), default_value=("" if new_value else " ")): log.debug('Got "{}" from {}.'.format(node, change_path)) change_nodes.append(node) except YAMLPathException as ex: log.critical(ex, 1) if len(change_nodes) == 1: # When there is exactly one result, its old format can be known. This # is necessary to retain whether the replacement value should be # represented later as a multi-line string when the new value is to be # encrypted. old_format = YAMLValueFormats.from_node(change_nodes[0]) log.debug("Collected nodes:") log.debug(change_nodes) # Check the value(s), if desired if args.check: for node in change_nodes: if processor.is_eyaml_value(node): # Sanity check: If either --publickey or --privatekey were set # then they must both be set in order to decrypt this value. # This is enforced only when the value must be decrypted due to # a --check request. if ((args.publickey and not args.privatekey) or (args.privatekey and not args.publickey)): log.error( "Neither or both private and public EYAML keys must be" + " set when --check is required to decrypt the old" + " value.") exit(1) try: check_value = processor.decrypt_eyaml(node) except EYAMLCommandException as ex: log.critical(ex, 1) else: check_value = node if not args.check == check_value: log.critical( '"{}" does not match the check value.'.format(args.check), 20) # Save the old value, if desired and possible if args.saveto: # Only one can be saved; otherwise it is impossible to meaningfully # convey to the end-user from exactly which other YAML node each saved # value came. if len(change_nodes) > 1: log.critical( "It is impossible to meaningly save more than one matched" + " value. Please omit --saveto or set --change to affect" + " exactly one value.", 1) saveto_path = YAMLPath(args.saveto, pathsep=args.pathsep) log.verbose("Saving the old value to {}.".format(saveto_path)) # Folded EYAML values have their embedded newlines converted to spaces # when read. As such, writing them back out breaks their original # format, despite being properly typed. To restore the original # written form, reverse the conversion, here. old_value = change_nodes[0] if ((old_format is YAMLValueFormats.FOLDED or old_format is YAMLValueFormats.LITERAL) and EYAMLProcessor.is_eyaml_value(old_value)): old_value = old_value.replace(" ", "\n") try: processor.set_value(saveto_path, clone_node(old_value), value_format=old_format) except YAMLPathException as ex: log.critical(ex, 1) # Set the requested value log.verbose("Setting the new value for {}.".format(change_path)) if args.eyamlcrypt: # If the user hasn't specified a format, use the same format as the # value being replaced, if known. format_type = YAMLValueFormats.from_str(args.format) if format_type is YAMLValueFormats.DEFAULT: format_type = old_format output_type = EYAMLOutputFormats.STRING if format_type in [YAMLValueFormats.FOLDED, YAMLValueFormats.LITERAL]: output_type = EYAMLOutputFormats.BLOCK try: processor.set_eyaml_value(change_path, new_value, output=output_type, mustexist=False) except EYAMLCommandException as ex: log.critical(ex, 2) else: processor.set_value(change_path, new_value, value_format=args.format) # Save a backup of the original file, if requested if args.backup: log.verbose("Saving a backup of {} to {}.".format( args.yaml_file, backup_file)) if exists(backup_file): remove(backup_file) copy2(args.yaml_file, backup_file) # Save the changed file log.verbose("Writing changed data to {}.".format(args.yaml_file)) with open(args.yaml_file, 'w') as yaml_dump: yaml.dump(yaml_data, yaml_dump)
def main(): """Main code.""" # Process any command-line arguments args = processcli() log = ConsolePrinter(args) validateargs(args, log) search_values = True search_keys = False include_key_aliases = False include_value_aliases = False if args.onlykeynames: search_values = False search_keys = True elif args.keynames: search_keys = True if args.include_aliases is IncludeAliases.INCLUDE_ALL_ALIASES: include_key_aliases = True include_value_aliases = True elif args.include_aliases is IncludeAliases.INCLUDE_KEY_ALIASES: include_key_aliases = True elif args.include_aliases is IncludeAliases.INCLUDE_VALUE_ALIASES: include_value_aliases = True # Prepare the YAML processor yaml = Parsers.get_yaml_editor() processor = EYAMLProcessor( log, None, binary=args.eyaml, publickey=args.publickey, privatekey=args.privatekey) # Process the input file(s) exit_state = 0 file_tally = -1 consumed_stdin = False for yaml_file in args.yaml_files: file_tally += 1 if yaml_file.strip() == "-": consumed_stdin = True log.debug( "yaml_merge::main: Processing file, {}".format( "STDIN" if yaml_file.strip() == "-" else yaml_file)) proc_state = process_yaml_file( args, yaml, log, yaml_file, processor, search_values, search_keys, include_key_aliases, include_value_aliases, file_tally ) if proc_state != 0: exit_state = proc_state # Check for a waiting STDIN document if (exit_state == 0 and not consumed_stdin and not args.nostdin and not sys.stdin.isatty() ): file_tally += 1 exit_state = process_yaml_file( args, yaml, log, "-", processor, search_values, search_keys, include_key_aliases, include_value_aliases, file_tally ) sys.exit(exit_state)
def main(): """Main code.""" # Process any command-line arguments args = processcli() log = ConsolePrinter(args) validateargs(args, log) search_values = True search_keys = False include_key_aliases = False include_value_aliases = False if args.onlykeynames: search_values = False search_keys = True elif args.keynames: search_keys = True if args.include_aliases is IncludeAliases.INCLUDE_ALL_ALIASES: include_key_aliases = True include_value_aliases = True elif args.include_aliases is IncludeAliases.INCLUDE_KEY_ALIASES: include_key_aliases = True elif args.include_aliases is IncludeAliases.INCLUDE_VALUE_ALIASES: include_value_aliases = True # Prepare the YAML processor yaml = get_yaml_editor() processor = EYAMLProcessor(log, None, binary=args.eyaml, publickey=args.publickey, privatekey=args.privatekey) # Process the input file(s) exit_state = 0 # pylint: disable=too-many-nested-blocks for yaml_file in args.yaml_files: # Try to open the file yaml_data = get_yaml_data(yaml, log, yaml_file) if yaml_data is None: # An error message has already been logged exit_state = 3 continue # Process all searches processor.data = yaml_data yaml_paths = [] for expression in args.search: exterm = get_search_term(log, expression) log.debug(("yaml_paths::main:" + "converting search expression '{}' into '{}'").format( expression, exterm)) if exterm is None: exit_state = 1 continue for result in search_for_paths( log, processor, yaml_data, exterm, args.pathsep, search_values=search_values, search_keys=search_keys, search_anchors=args.refnames, include_key_aliases=include_key_aliases, include_value_aliases=include_value_aliases, decrypt_eyaml=args.decrypt, expand_children=args.expand): # Record only unique results add_entry = True for entry in yaml_paths: if str(result) == str(entry[1]): add_entry = False break if add_entry: yaml_paths.append((expression, result)) if not yaml_paths: # Nothing further to do when there are no results continue if args.except_expression: for expression in args.except_expression: exterm = get_search_term(log, expression) log.debug( ("yaml_paths::main:" + "converted except expression '{}' into '{}'").format( expression, exterm)) if exterm is None: exit_state = 1 continue for result in search_for_paths( log, processor, yaml_data, exterm, args.pathsep, search_values=search_values, search_keys=search_keys, search_anchors=args.refnames, include_key_aliases=include_key_aliases, include_value_aliases=include_value_aliases, decrypt_eyaml=args.decrypt, expand_children=args.expand): for entry in yaml_paths: if str(result) == str(entry[1]): yaml_paths.remove(entry) break # Entries are already unique print_results(args, processor, yaml_file, yaml_paths) exit(exit_state)
def test_good_multi_replacements(self, script_runner, tmp_path_factory, old_eyaml_keys, new_eyaml_keys, quiet_logger): from yamlpath.func import unwrap_node_coords from yamlpath.common import Parsers from yamlpath import Processor from yamlpath.eyaml import EYAMLProcessor simple_content = """--- encrypted_string: ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAHA4rPcTzvgzPLtnGz3yoyX/kVlQ5TnPXcScXK2bwjguGZLkuzv/JVPAsOm4t6GlnROpy4zb/lUMHRJDChJhPLrSj919B8//huoMgw0EU5XTcaN6jeDDjL+vhjswjvLFOux66UwvMo8sRci/e2tlFiam8VgxzV0hpF2qRrL/l84V04gL45kq4PCYDWrJNynOwYVbSIF+qc5HaF25H8kHq1lD3RB6Ob/J942Q7k5Qt7W9mNm9cKZmxwgtUgIZWXW6mcPJ2dXDB/RuPJJSrLsb1VU/DkhdgxaNzvLCA+MViyoFUkCfHFNZbaHKNkoYXBy7dLmoh/E5tKv99FeG/7CzL3DBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBCVU5Mjt8+4dLkoqB9YArfkgCDkdIhXR9T1M4YYa1qTE6by61VPU3g1aMExRmo4tNZ8FQ==] encrypted_block: > ENC[PKCS7,MIIBeQYJKoZIhvcNAQcDoIIBajCCAWYCAQAxggEhMIIBHQIBADAFMAACAQEw DQYJKoZIhvcNAQEBBQAEggEAnxQVqyIgRTb/+VP4Q+DLJcnlS8YPouXEW8+z it9uwUA02CEPxCEU944GcHpgTY3EEtkm+2Z/jgXI119VMML+OOQ1NkwUiAw/ wq0vwz2D16X31XzhedQN5FZbfZ1C+2tWSQfCjE0bu7IeHfyR+k2ssD11kNZh JDEr2bM2dwOdT0y7VGcQ06vI9gw6UXcwYAgS6FoLm7WmFftjcYiNB+0EJSW0 VcTn2gveaw9iOQcum/Grby+9Ybs28fWd8BoU+ZWDpoIMEceujNa9okIXNPJO jcvv1sgauwJ3RX6WFQIy/beS2RT5EOLhWIZCAQCcgJWgovu3maB7dEUZ0NLG OYUR7zA8BgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBAbO16EzQ5/cdcvgB0g tpKIgBAEgTLT5n9Jtc9venK0CKso] """ anchored_content = """--- aliases: - &blockStyle > ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEw DQYJKoZIhvcNAQEBBQAEggEArvk6OYa1gACTdrWq2SpCrtGRlc61la5AGU7L aLTyKfqD9vqx71RDjobfOF96No07kLsEpoAJ+LKKHNjdG6kjvpGPmttj9Dkm XVoU6A+YCmm4iYFKD/NkoSOEyAkoDOXSqdjrgt0f37GefEsXt6cqAavDpUJm pmc0KI4TCG5zpfCxqttMs+stOY3Y+0WokkulQujZ7K3SdWUSHIysgMrWiect Wdg5unxN1A/aeyvhgvYSNPjU9KBco7SDnigSs9InW/QghJFrZRrDhTp1oTUc qK5lKvaseHkVGi91vPWeLQxZt1loJB5zL6j5BxMbvRfJK+wc3ax2u4x8WTAB EurCwzBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBAwcy7jvcOGcMfLEtug LEXbgCBkocdckuDe14mVGmUmM++xN34OEVRCeGVWWUnWq1DJ4Q==] - &stringStyle ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAIu44u62q5sVfzC7kytLi2Z/EzH2DKr4vDsoqDBeSZ71aRku/uSrjyiO4lyoq9Kva+eBAyjBay5fnqPVBaU3Rud2pdEoZEoyofi02jn4hxUKpAO1W0AUgsQolGe53qOdM4U8RbwnTR0gr3gp2mCd18pH3SRMP9ryrsBAxGzJ6mR3RgdZnlTlqVGXCeWUeVpbH+lcHw3uvd+o/xkvJ/3ypxz+rWILiAZ3QlCirzn/qb2fHuKf3VBh8RVFuQDaM5voajZlgjD6KzNCsbATOqOA6eJI4j0ngPdDlIjGHAnahuyluQ5f5SIaIjLC+ZeCOfIYni0MQ+BHO0JNbccjq2Unb7TBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBCYmAI0Ao3Ok1cSmVw0SgQGgCBK62z1r5RfRjf1xKfqDxTsGUHfsUmM3EjGJfnWzCRvuQ==] block: *blockStyle string: *stringStyle yet_another: 'more.complex.child': *blockStyle """ simple_file = create_temp_yaml_file(tmp_path_factory, simple_content) anchored_file = create_temp_yaml_file(tmp_path_factory, anchored_content) result = script_runner.run( self.command, "--newprivatekey={}".format(new_eyaml_keys[0]), "--newpublickey={}".format(new_eyaml_keys[1]), "--oldprivatekey={}".format(old_eyaml_keys[0]), "--oldpublickey={}".format(old_eyaml_keys[1]), simple_file, anchored_file ) assert result.success, result.stderr with open(simple_file, 'r') as fhnd: simple_data = fhnd.read() with open(anchored_file, 'r') as fhnd: anchored_data = fhnd.read() assert not simple_data == simple_content assert not anchored_data == anchored_content # Verify that block and string formatting is correct yaml = Parsers.get_yaml_editor() (yaml_rotated_data, doc_loaded) = Parsers.get_yaml_data( yaml, quiet_logger, anchored_data, literal=True) if not doc_loaded: # An error message has already been logged assert False, "Rotated anchored data failed to load" source_processor = Processor(quiet_logger, yaml_rotated_data) for node in source_processor.get_nodes('/block', mustexist=True): assert not ' ' in unwrap_node_coords(node) # Test that the pre- and post-rotated values are identical (yaml_anchored_data, doc_loaded) = Parsers.get_yaml_data( yaml, quiet_logger, anchored_content, literal=True) if not doc_loaded: # An error message has already been logged assert False, "Original anchored data failed to load" (yaml_rotated_data, doc_loaded) = Parsers.get_yaml_data( yaml, quiet_logger, anchored_data, literal=True) if not doc_loaded: # An error message has already been logged assert False, "Rotated anchored data failed to load" source_processor = EYAMLProcessor( quiet_logger, yaml_anchored_data, privatekey=old_eyaml_keys[0], publickey=old_eyaml_keys[1]) for node in source_processor.get_eyaml_values( '/block', True ): assert unwrap_node_coords(node) == 'This is a test value.' rotated_processor = EYAMLProcessor( quiet_logger, yaml_rotated_data, privatekey=new_eyaml_keys[0], publickey=new_eyaml_keys[1]) for node in rotated_processor.get_eyaml_values( '/block', True ): assert unwrap_node_coords(node) == 'This is a test value.'
def search_for_paths(logger: ConsolePrinter, processor: EYAMLProcessor, data: Any, terms: SearchTerms, pathsep: PathSeperators = PathSeperators.DOT, build_path: str = "", seen_anchors: Optional[List[str]] = None, **kwargs: bool) -> Generator[YAMLPath, None, None]: """ Recursively search a data structure for nodes matching an expression. The nodes can be keys, values, and/or elements. When dealing with anchors and their aliases, the caller indicates whether to include only the original anchor or the anchor and all of its (duplicate) aliases. """ search_values: bool = kwargs.pop("search_values", True) search_keys: bool = kwargs.pop("search_keys", False) search_anchors: bool = kwargs.pop("search_anchors", False) include_key_aliases: bool = kwargs.pop("include_key_aliases", True) include_value_aliases: bool = kwargs.pop("include_value_aliases", False) decrypt_eyaml: bool = kwargs.pop("decrypt_eyaml", False) expand_children: bool = kwargs.pop("expand_children", False) strsep = str(pathsep) invert = terms.inverted method = terms.method term = terms.term if seen_anchors is None: seen_anchors = [] if isinstance(data, CommentedSeq): # Build the path if not build_path and pathsep is PathSeperators.FSLASH: build_path = strsep build_path += "[" for idx, ele in enumerate(data): # Any element may or may not have an Anchor/Alias anchor_matched = Searches.search_anchor( ele, terms, seen_anchors, search_anchors=search_anchors, include_aliases=include_value_aliases) logger.debug( ("yaml_paths::search_for_paths<list>:" + "anchor search => {}.") .format(anchor_matched) ) # Build the temporary YAML Path using either Anchor or Index if anchor_matched is AnchorMatches.NO_ANCHOR: # Not an anchor/alias, so ref this node by its index tmp_path = build_path + str(idx) + "]" else: tmp_path = "{}&{}]".format( build_path, YAMLPath.escape_path_section(ele.anchor.value, pathsep) ) if anchor_matched is AnchorMatches.ALIAS_EXCLUDED: continue if anchor_matched in [AnchorMatches.MATCH, AnchorMatches.ALIAS_INCLUDED]: logger.debug( ("yaml_paths::search_for_paths<list>:" + "yielding an Anchor/Alias match, {}.") .format(tmp_path) ) if expand_children: for path in yield_children( logger, ele, terms, pathsep, tmp_path, seen_anchors, search_anchors=search_anchors, include_key_aliases=include_key_aliases, include_value_aliases=include_value_aliases): yield path else: yield YAMLPath(tmp_path) continue if isinstance(ele, (CommentedSeq, CommentedMap)): logger.debug( "Recursing into complex data:", data=ele, prefix="yaml_paths::search_for_paths<list>: ", footer=">>>> >>>> >>>> >>>> >>>> >>>> >>>>") for subpath in search_for_paths( logger, processor, ele, terms, pathsep, tmp_path, seen_anchors, search_values=search_values, search_keys=search_keys, search_anchors=search_anchors, include_key_aliases=include_key_aliases, include_value_aliases=include_value_aliases, decrypt_eyaml=decrypt_eyaml, expand_children=expand_children ): logger.debug( "Yielding RECURSED match, {}.".format(subpath), prefix="yaml_paths::search_for_paths<list>: ", footer="<<<< <<<< <<<< <<<< <<<< <<<< <<<<" ) yield subpath elif search_values: if (anchor_matched is AnchorMatches.UNSEARCHABLE_ALIAS and not include_value_aliases): continue check_value = ele if decrypt_eyaml and processor.is_eyaml_value(ele): check_value = processor.decrypt_eyaml(ele) matches = Searches.search_matches(method, term, check_value) if (matches and not invert) or (invert and not matches): logger.debug( ("yaml_paths::search_for_paths<list>:" + "yielding VALUE match, {}: {}." ).format(check_value, tmp_path) ) yield YAMLPath(tmp_path) # pylint: disable=too-many-nested-blocks elif isinstance(data, CommentedMap): if build_path: build_path += strsep elif pathsep is PathSeperators.FSLASH: build_path = strsep pool = data.non_merged_items() if include_key_aliases or include_value_aliases: pool = data.items() for key, val in pool: tmp_path = build_path + YAMLPath.escape_path_section(key, pathsep) # Search the value anchor to have it on record, in case the key # anchor match would otherwise block the value anchor from # appearing in seen_anchors (which is important). val_anchor_matched = Searches.search_anchor( val, terms, seen_anchors, search_anchors=search_anchors, include_aliases=include_value_aliases) logger.debug( ("yaml_paths::search_for_paths<dict>:" + "VALUE anchor search => {}.") .format(val_anchor_matched) ) # Search the key when the caller wishes it. if search_keys: # The key itself may be an Anchor or Alias. Search it when the # caller wishes. key_anchor_matched = Searches.search_anchor( key, terms, seen_anchors, search_anchors=search_anchors, include_aliases=include_key_aliases) logger.debug( ("yaml_paths::search_for_paths<dict>:" + "KEY anchor search, {}: {}.") .format(key, key_anchor_matched) ) if key_anchor_matched in [AnchorMatches.MATCH, AnchorMatches.ALIAS_INCLUDED]: logger.debug( ("yaml_paths::search_for_paths<dict>:" + "yielding a KEY-ANCHOR match, {}." ).format(key, tmp_path) ) if expand_children: for path in yield_children( logger, val, terms, pathsep, tmp_path, seen_anchors, search_anchors=search_anchors, include_key_aliases=include_key_aliases, include_value_aliases=include_value_aliases): yield path else: yield YAMLPath(tmp_path) continue # Search the name of the key, itself matches = Searches.search_matches(method, term, key) if (matches and not invert) or (invert and not matches): logger.debug( ("yaml_paths::search_for_paths<dict>:" + "yielding KEY name match, {}: {}." ).format(key, tmp_path) ) if expand_children: # Include every non-excluded child node under this # matched parent node. for path in yield_children( logger, val, terms, pathsep, tmp_path, seen_anchors, search_anchors=search_anchors, include_key_aliases=include_key_aliases, include_value_aliases=include_value_aliases): yield path else: # No other matches within this node matter because they # are already in the result. yield YAMLPath(tmp_path) continue # The value may itself be anchored; search it if requested if val_anchor_matched is AnchorMatches.ALIAS_EXCLUDED: continue if val_anchor_matched in [AnchorMatches.MATCH, AnchorMatches.ALIAS_INCLUDED]: logger.debug( ("yaml_paths::search_for_paths<dict>:" + "yielding a VALUE-ANCHOR match, {}.") .format(tmp_path) ) if expand_children: for path in yield_children( logger, val, terms, pathsep, tmp_path, seen_anchors, search_anchors=search_anchors, include_key_aliases=include_key_aliases, include_value_aliases=include_value_aliases): yield path else: yield YAMLPath(tmp_path) continue if isinstance(val, (CommentedSeq, CommentedMap)): logger.debug( "Recursing into complex data:", data=val, prefix="yaml_paths::search_for_paths<dict>: ", footer=">>>> >>>> >>>> >>>> >>>> >>>> >>>>" ) for subpath in search_for_paths( logger, processor, val, terms, pathsep, tmp_path, seen_anchors, search_values=search_values, search_keys=search_keys, search_anchors=search_anchors, include_key_aliases=include_key_aliases, include_value_aliases=include_value_aliases, decrypt_eyaml=decrypt_eyaml, expand_children=expand_children ): logger.debug( "Yielding RECURSED match, {}.".format(subpath), prefix="yaml_paths::search_for_paths<dict>: ", footer="<<<< <<<< <<<< <<<< <<<< <<<< <<<<" ) yield subpath elif search_values: if (val_anchor_matched is AnchorMatches.UNSEARCHABLE_ALIAS and not include_value_aliases): continue check_value = val if decrypt_eyaml and processor.is_eyaml_value(val): check_value = processor.decrypt_eyaml(val) matches = Searches.search_matches(method, term, check_value) if (matches and not invert) or (invert and not matches): logger.debug( ("yaml_paths::search_for_paths<dict>:" + "yielding VALUE match, {}: {}." ).format(check_value, tmp_path) ) yield YAMLPath(tmp_path)
def test_impossible_eyaml_exe(self, exe): assert None == EYAMLProcessor.get_eyaml_executable(exe)
def main(): """Main code.""" args = processcli() log = ConsolePrinter(args) validateargs(args, log) change_path = YAMLPath(args.change, pathsep=args.pathsep) must_exist = args.mustexist or args.saveto # Obtain the replacement value consumed_stdin = False new_value = None has_new_value = False if args.value or args.value == "": new_value = args.value has_new_value = True elif args.stdin: new_value = ''.join(sys.stdin.readlines()) consumed_stdin = True has_new_value = True elif args.file: with open(args.file, 'r') as fhnd: new_value = fhnd.read().rstrip() has_new_value = True elif args.null: new_value = None has_new_value = True elif args.random is not None: new_value = ''.join( secrets.choice(args.random_from) for _ in range(args.random)) has_new_value = True # Prep the YAML parser yaml = Parsers.get_yaml_editor() # Attempt to open the YAML file; check for parsing errors if args.yaml_file: yaml_data = _try_load_input_file(args, log, yaml, change_path, new_value) if args.yaml_file.strip() == '-': consumed_stdin = True # Check for a waiting STDIN document if (not consumed_stdin and not args.yaml_file and not args.nostdin and not sys.stdin.isatty()): args.yaml_file = "-" yaml_data = _try_load_input_file(args, log, yaml, change_path, new_value) # Load the present nodes at the specified YAML Path processor = EYAMLProcessor(log, yaml_data, binary=args.eyaml, publickey=args.publickey, privatekey=args.privatekey) change_node_coordinates = _get_nodes( log, processor, change_path, must_exist=must_exist, default_value=("" if new_value else " ")) old_format = YAMLValueFormats.DEFAULT if len(change_node_coordinates) == 1: # When there is exactly one result, its old format can be known. This # is necessary to retain whether the replacement value should be # represented later as a multi-line string when the new value is to be # encrypted. old_format = YAMLValueFormats.from_node( change_node_coordinates[0].node) # Check the value(s), if desired if args.check: for node_coordinate in change_node_coordinates: if processor.is_eyaml_value(node_coordinate.node): # Sanity check: If either --publickey or --privatekey were set # then they must both be set in order to decrypt this value. # This is enforced only when the value must be decrypted due to # a --check request. if ((args.publickey and not args.privatekey) or (args.privatekey and not args.publickey)): log.error( "Neither or both private and public EYAML keys must be" + " set when --check is required to decrypt the old" + " value.") sys.exit(1) try: check_value = processor.decrypt_eyaml(node_coordinate.node) except EYAMLCommandException as ex: log.critical(ex, 1) else: check_value = node_coordinate.node if not args.check == check_value: log.critical( '"{}" does not match the check value.'.format(args.check), 20) # Save the old value, if desired and possible if args.saveto: # Only one can be saved; otherwise it is impossible to meaningfully # convey to the end-user from exactly which other YAML node each saved # value came. if len(change_node_coordinates) > 1: log.critical( "It is impossible to meaningly save more than one matched" + " value. Please omit --saveto or set --change to affect" + " exactly one value.", 1) saveto_path = YAMLPath(args.saveto, pathsep=args.pathsep) log.verbose("Saving the old value to {}.".format(saveto_path)) # Folded EYAML values have their embedded newlines converted to spaces # when read. As such, writing them back out breaks their original # format, despite being properly typed. To restore the original # written form, reverse the conversion, here. old_value = change_node_coordinates[0].node if ((old_format is YAMLValueFormats.FOLDED or old_format is YAMLValueFormats.LITERAL) and EYAMLProcessor.is_eyaml_value(old_value)): old_value = old_value.replace(" ", "\n") try: processor.set_value(saveto_path, Nodes.clone_node(old_value), value_format=old_format, tag=args.tag) except YAMLPathException as ex: log.critical(ex, 1) # Set the requested value log.verbose("Applying changes to {}.".format(change_path)) if args.delete: # Destroy the collected nodes (from their parents) in the reverse order # they were discovered. This is necessary lest Array elements be # improperly handled, leading to unwanted data loss. _delete_nodes(log, processor, change_node_coordinates) elif args.aliasof: # Assign the change nodes as Aliases of whatever --aliasof points to _alias_nodes(log, processor, change_node_coordinates, args.aliasof, args.anchor) elif args.eyamlcrypt: # If the user hasn't specified a format, use the same format as the # value being replaced, if known. format_type = YAMLValueFormats.from_str(args.format) if format_type is YAMLValueFormats.DEFAULT: format_type = old_format output_type = EYAMLOutputFormats.STRING if format_type in [YAMLValueFormats.FOLDED, YAMLValueFormats.LITERAL]: output_type = EYAMLOutputFormats.BLOCK try: processor.set_eyaml_value(change_path, new_value, output=output_type, mustexist=False) except EYAMLCommandException as ex: log.critical(ex, 2) elif has_new_value: try: processor.set_value(change_path, new_value, value_format=args.format, mustexist=must_exist, tag=args.tag) except YAMLPathException as ex: log.critical(ex, 1) elif args.tag: _tag_nodes(processor.data, args.tag, change_node_coordinates) # Write out the result write_output_document(args, log, yaml, yaml_data)
def test_not_can_run_eyaml(self, quiet_logger): processor = EYAMLProcessor(quiet_logger, None) processor.eyaml = None assert False == processor._can_run_eyaml()