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
Exemplo n.º 2
0
    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))
Exemplo n.º 3
0
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)
Exemplo n.º 4
0
    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
Exemplo n.º 5
0
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)
Exemplo n.º 6
0
    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")
Exemplo n.º 7
0
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)
Exemplo n.º 8
0
    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
Exemplo n.º 9
0
    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)
Exemplo n.º 10
0
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)
Exemplo n.º 11
0
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)
Exemplo n.º 12
0
 def test_none_eyaml_value(self):
     assert False == EYAMLProcessor.is_eyaml_value(None)
Exemplo n.º 13
0
 def test_decrypt_calledprocesserror(self, quiet_logger, force_subprocess_run_cpe):
     processor = EYAMLProcessor(quiet_logger, None)
     with pytest.raises(EYAMLCommandException):
         processor.decrypt_eyaml("ENC[...]")
Exemplo n.º 14
0
 def test_non_executable(self, old_eyaml_keys, force_no_access):
     assert EYAMLProcessor.get_eyaml_executable(str(old_eyaml_keys[0])) is None
Exemplo n.º 15
0
 def test_ignore_already_decrypted_cryps(self, quiet_logger):
     processor = EYAMLProcessor(quiet_logger, None)
     testval = "some value"
     assert testval == processor.decrypt_eyaml(testval)
Exemplo n.º 16
0
 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)
Exemplo n.º 17
0
 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[...]")
Exemplo n.º 18
0
 def test_ignore_already_encrypted_cryps(self, quiet_logger):
     processor = EYAMLProcessor(quiet_logger, None)
     testval = "ENC[...]"
     assert testval == processor.encrypt_eyaml(testval)
Exemplo n.º 19
0
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."""
Exemplo n.º 20
0
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)
Exemplo n.º 21
0
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)
Exemplo n.º 22
0
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)
Exemplo n.º 23
0
    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.'
Exemplo n.º 24
0
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)
Exemplo n.º 25
0
 def test_impossible_eyaml_exe(self, exe):
     assert None == EYAMLProcessor.get_eyaml_executable(exe)
Exemplo n.º 26
0
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)
Exemplo n.º 27
0
 def test_not_can_run_eyaml(self, quiet_logger):
     processor = EYAMLProcessor(quiet_logger, None)
     processor.eyaml = None
     assert False == processor._can_run_eyaml()