def main(argv=None):
    """Main process."""

    # Parse command line arguments.
    argparser = build_argument_parser()
    args = argparser.parse_args(argv)

    retval = 0
    for filename in args.filenames:
        try:
            with open(filename, "rb") as openfile:
                manifest = json.load(openfile)
        except json.decoder.JSONDecodeError as err:
            print("{}: json parsing error: {}".format(filename, err))
            retval = 1
            break  # No need to continue checking this file

        # Check for presence of required keys.
        required_keys = ("title", "properties", "description")
        if not validate_required_keys(manifest, filename, required_keys):
            retval = 1
            break  # No need to continue checking this file

        # Ensure top level keys and their list items have expected types.
        if not validate_key_types("<root>", manifest, filename):
            retval = 1
        if not validate_list_item_types("<root>", manifest, filename):
            retval = 1

        # Run checks recursively for all properties
        if "properties" in manifest:
            if not validate_properties(manifest["properties"], filename):
                retval = 1

    return retval
def main(argv=None):
    """Main process."""

    # Typical extensions for installer packages.
    pkg_exts = ("pkg", "dmg")
    dupe_suffixes = [
        "__{}.{}".format(i, ext) for ext in pkg_exts for i in range(1, 9)
    ]

    # RestartAction values that obviate the need to check blocking applications.
    blocking_actions = ("RequireRestart", "RequireShutdown", "RequireLogout")

    # Parse command line arguments.
    argparser = build_argument_parser()
    args = argparser.parse_args(argv)

    retval = 0
    for filename in args.filenames:
        try:
            with open(filename, "rb") as openfile:
                pkginfo = plistlib.load(openfile)
        except (ExpatError, ValueError) as err:
            print("{}: plist parsing error: {}".format(filename, err))
            retval = 1

        # Check for presence of required pkginfo keys.
        if args.required_keys:
            if not validate_required_keys(pkginfo, filename,
                                          args.required_keys):
                retval = 1
                break  # No need to continue checking this file

        # Ensure pkginfo keys have expected types.
        if not validate_pkginfo_key_types(pkginfo, filename):
            retval = 1

        # Validate RestartAction key.
        if not validate_restart_action_key(pkginfo, filename):
            retval = 1

        # Check for common mistakes in min/max OS version keys.
        os_vers_corrections = {
            "min_os": "minimum_os_version",
            "max_os": "maximum_os_version",
            "min_os_vers": "minimum_os_version",
            "max_os_vers": "maximum_os_version",
            "minimum_os": "minimum_os_version",
            "maximum_os": "maximum_os_version",
            "minimum_os_vers": "minimum_os_version",
            "maximum_os_vers": "maximum_os_version",
        }
        for os_vers_key in os_vers_corrections:
            if os_vers_key in pkginfo:
                print("{}: You used {} when you probably meant {}.".format(
                    filename, os_vers_key, os_vers_corrections[os_vers_key]))
                retval = 1

        # Check for rogue categories.
        if args.categories and pkginfo.get("category") not in args.categories:
            print('{}: category "{}" is not in list of approved categories'.
                  format(filename, pkginfo.get("category")))
            retval = 1

        # Check for rogue catalogs.
        if args.catalogs:
            for catalog in pkginfo.get("catalogs"):
                if catalog not in args.catalogs:
                    print('{}: catalog "{}" is not in approved list'.format(
                        filename, catalog))
                    retval = 1

        # Check for pkg filenames showing signs of duplicate imports.
        if pkginfo.get("installer_item_location",
                       "").endswith(tuple(dupe_suffixes)):
            print('{}: installer item "{}" may be a duplicate import'.format(
                filename, pkginfo.get("installer_item_location")))
            retval = 1

        # Checking for the absence of blocking_applications for pkg installers.
        # If a pkg doesn't require blocking_applications, use empty "<array/>" in pkginfo.
        if all((
                "blocking_applications" not in pkginfo,
                pkginfo.get("installer_item_location", "").endswith(".pkg"),
                pkginfo.get("RestartAction") not in blocking_actions,
                not pkginfo["name"].startswith("munkitools"),
        )):
            print(
                "WARNING: {}: contains a pkg installer but has no blocking applications"
                .format(filename))

        # Ensure an icon exists for the item.
        if not any((
                pkginfo.get("icon_name"),
                os.path.isfile("icons/{}.png".format(pkginfo["name"])),
                pkginfo.get("installer_type") == "apple_update_metadata",
        )):
            print("{}: missing icon".format(filename))
            retval = 1

        # Ensure uninstall method is set correctly if uninstall_script exists.
        if "uninstall_script" in pkginfo:
            if pkginfo.get("uninstall_method") != "uninstall_script":
                print(
                    '{}: has uninstall script, but the uninstall method is set to "{}"'
                    .format(filename, pkginfo.get("uninstall_method")))
                retval = 1

        # Ensure all pkginfo scripts have a proper shebang.
        shebangs = (
            "#!/bin/bash",
            "#!/bin/sh",
            "#!/bin/zsh",
            "#!/usr/bin/osascript",
            "#!/usr/bin/perl",
            "#!/usr/bin/python",
            "#!/usr/bin/ruby",
            "#!/usr/local/munki/munki-python",
            "#!/usr/local/munki/Python.framework/Versions/Current/bin/python3",
        )
        script_types = (
            "installcheck_script",
            "uninstallcheck_script",
            "postinstall_script",
            "postuninstall_script",
            "preinstall_script",
            "preuninstall_script",
            "uninstall_script",
        )
        for script_type in script_types:
            if script_type in pkginfo:
                if all(not pkginfo[script_type].startswith(x + "\n")
                       for x in shebangs):
                    print(
                        "{}: Has a {} that does not start with a valid shebang."
                        .format(filename, script_type))
                    retval = 1

        # Ensure the items_to_copy list does not include trailing slashes.
        # Credit to @bruienne for this idea.
        # https://gist.github.com/bruienne/9baa958ec6dbe8f09d94#file-munki_fuzzinator-py-L211-L219
        if "items_to_copy" in pkginfo:
            for item_to_copy in pkginfo.get("items_to_copy"):
                if item_to_copy.get("destination_path").endswith("/"):
                    print(
                        '{}: has an items_to_copy with a trailing slash: "{}"'.
                        format(filename, item_to_copy["destination_path"]))
                    retval = 1

    return retval
def main(argv=None):
    """Main process."""

    # Parse command line arguments.
    argparser = build_argument_parser()
    args = argparser.parse_args(argv)
    if args.strict:
        args.ignore_min_vers_before = "0.1.0"

    # Track identifiers we've seen.
    seen_identifiers = []

    retval = 0
    for filename in args.filenames:

        recipe = load_autopkg_recipe(filename)
        if not recipe:
            retval = 1
            break  # No need to continue checking this file

        # For future implementation of validate_unused_input_vars()
        # with open(filename, "r") as openfile:
        #     recipe_text = openfile.read()

        # Top level keys that all AutoPkg recipes should contain.
        required_keys = ["Identifier"]
        if not validate_required_keys(recipe, filename, required_keys):
            retval = 1
            break  # No need to continue checking this file

        # Ensure the recipe identifier isn't duplicated.
        if recipe["Identifier"] in seen_identifiers:
            print(
                '{}: Identifier "{}" is shared by another recipe in this repo.'
                .format(filename, recipe["Identifier"]))
            retval = 1
        else:
            seen_identifiers.append(recipe["Identifier"])

        # Validate identifiers.
        if args.override_prefix and "Process" not in recipe:
            if not validate_recipe_prefix(recipe, filename,
                                          args.override_prefix):
                retval = 1
        if args.recipe_prefix and "Process" in recipe:
            if not validate_recipe_prefix(recipe, filename,
                                          args.recipe_prefix):
                retval = 1
        if recipe["Identifier"] == recipe.get("ParentRecipe"):
            print("{}: Identifier and ParentRecipe should not "
                  "be the same.".format(filename))
            retval = 1

        # Validate that all input variables are used.
        # (Disabled for now because it's a little too opinionated, and doesn't take into account
        # whether environmental variables are used in custom processors.)
        # if args.strict:
        #     if not validate_unused_input_vars(recipe, recipe_text, filename):
        #         retval = 1

        # If the Input key contains a pkginfo dict, make a best effort to validate its contents.
        input_key = recipe.get("Input", recipe.get("input",
                                                   recipe.get("INPUT")))
        if input_key and "pkginfo" in input_key:
            if not validate_pkginfo_key_types(input_key["pkginfo"], filename):
                retval = 1
            if not validate_restart_action_key(input_key["pkginfo"], filename):
                retval = 1

            # Check for common mistakes in min/max OS version keys
            os_vers_corrections = {
                "min_os": "minimum_os_version",
                "max_os": "maximum_os_version",
                "min_os_vers": "minimum_os_version",
                "max_os_vers": "maximum_os_version",
                "minimum_os": "minimum_os_version",
                "maximum_os": "maximum_os_version",
                "minimum_os_vers": "minimum_os_version",
                "maximum_os_vers": "maximum_os_version",
            }
            for os_vers_key in os_vers_corrections:
                if os_vers_key in input_key["pkginfo"]:
                    print("{}: You used {} when you probably meant {}.".format(
                        filename, os_vers_key,
                        os_vers_corrections[os_vers_key]))
                    retval = 1

            # TODO: Additional pkginfo checks here.

        # Warn about comments that would be lost during `plutil -convert xml1`
        if not validate_comments(filename, args.strict):
            retval = 1

        # Processor checks.
        if "Process" in recipe:
            process = recipe["Process"]

            if not validate_processor_keys(process, filename):
                retval = 1

            if not validate_endofcheckphase(process, filename):
                retval = 1

            if not validate_no_var_in_app_path(process, filename):
                retval = 1

            min_vers = recipe.get("MinimumVersion")
            if min_vers and not validate_minimumversion(
                    process, min_vers, args.ignore_min_vers_before, filename):
                retval = 1

            if not validate_no_deprecated_procs(process, filename):
                retval = 1

            if not validate_no_superclass_procs(process, filename):
                retval = 1

            if not validate_jamf_processor_order(process, filename):
                retval = 1

            if HAS_AUTOPKGLIB:
                if not validate_proc_args(process, filename):
                    retval = 1

            if args.strict:
                if not validate_proc_type_conventions(process, filename):
                    retval = 1

                if not validate_required_proc_for_types(process, filename):
                    retval = 1

    return retval
Exemplo n.º 4
0
def main(argv=None):
    """Main process."""

    # Parse command line arguments.
    argparser = build_argument_parser()
    args = argparser.parse_args(argv)

    retval = 0
    buildinfo = {}
    for filename in args.filenames:
        if filename.endswith(".plist"):
            try:
                with open(filename, "rb") as openfile:
                    buildinfo = plistlib.load(openfile)
            except (ExpatError, ValueError) as err:
                print("{}: plist parsing error: {}".format(filename, err))
                retval = 1
                break  # no need to continue testing this file
        elif filename.endswith((".yaml", ".yml")):
            try:
                with open(filename, "r") as openfile:
                    buildinfo = yaml.load(openfile)
            except Exception as err:
                print("{}: yaml parsing error: {}".format(filename, err))
                retval = 1
                break  # no need to continue testing this file
        elif filename.endswith(".json"):
            try:
                with open(filename, "r") as openfile:
                    buildinfo = json.load(openfile)
            except Exception as err:
                print("{}: json parsing error: {}".format(filename, err))
                retval = 1
                break  # no need to continue testing this file

        if not buildinfo or not isinstance(buildinfo, dict):
            print("{}: cannot parse build-info file".format(filename))
            retval = 1
            break

        # Top level keys that all build-info files should contain.
        # NOTE: Even though other keys are listed as non-"optional" in the documentation,
        # name and version appear to be the only ones that are actually required.
        required_keys = ("name", "version")
        if not validate_required_keys(buildinfo, filename, required_keys):
            retval = 1
            break  # No need to continue checking this file

        if args.identifier_prefix:
            # Warn if the identifier does not start with the expected prefix.
            if not buildinfo.get("identifier", "").startswith(
                    args.identifier_prefix):
                print("{}: identifier does not start "
                      "with {}.".format(filename, args.identifier_prefix))
                retval = 1

        # Ensure buildinfo keys have expected types.
        if not validate_buildinfo_key_types(buildinfo, filename):
            retval = 1

        # Warn if install_location is not the startup disk.
        if buildinfo.get("install_location") != "/":
            print("{}: WARNING: install_location is not set to the "
                  "startup disk.".format(filename))

    return retval
def main(argv=None):
    """Main process."""

    # Typical extensions for installer packages.
    pkg_exts = ("pkg", "dmg")
    dupe_suffixes = [
        "__{}.{}".format(i, ext) for ext in pkg_exts for i in range(1, 9)
    ]

    # RestartAction values that obviate the need to check blocking applications.
    blocking_actions = ("RequireRestart", "RequireShutdown", "RequireLogout")

    # Parse command line arguments.
    argparser = build_argument_parser()
    args = argparser.parse_args(argv)

    retval = 0
    for filename in args.filenames:
        try:
            pkginfo = plistlib.readPlist(filename)
        except (ExpatError, ValueError) as err:
            print("{}: plist parsing error: {}".format(filename, err))
            retval = 1

        # Check for presence of required pkginfo keys.
        if args.required_keys:
            if not validate_required_keys(pkginfo, filename,
                                          args.required_keys):
                retval = 1
                break  # No need to continue checking this file

        # Ensure pkginfo keys have expected types.
        if not validate_pkginfo_key_types(pkginfo, filename):
            retval = 1

        # Check for rogue categories.
        if args.categories and pkginfo.get("category") not in args.categories:
            print('{}: category "{}" is not in list of approved categories'.
                  format(filename, pkginfo.get("category")))
            retval = 1

        # Check for rogue catalogs.
        if args.catalogs:
            for catalog in pkginfo.get("catalogs"):
                if catalog not in args.catalogs:
                    print('{}: catalog "{}" is not in approved list'.format(
                        filename, catalog))
                    retval = 1

        # Check for pkg filenames showing signs of duplicate imports.
        if pkginfo.get("installer_item_location",
                       "").endswith(tuple(dupe_suffixes)):
            print('{}: installer item "{}" may be a duplicate import'.format(
                filename, pkginfo.get("installer_item_location")))
            retval = 1

        # Checking for the absence of blocking_applications for pkg installers.
        # If a pkg doesn't require blocking_applications, use empty "<array/>" in pkginfo.
        if all((
                "blocking_applications" not in pkginfo,
                pkginfo.get("installer_item_location", "").endswith(".pkg"),
                pkginfo.get("RestartAction") not in blocking_actions,
                not pkginfo["name"].startswith("munkitools"),
        )):
            print(
                "{}: contains a pkg installer but has no blocking applications"
                .format(filename))
            retval = 1

        # Ensure an icon exists for the item.
        if not any((
                pkginfo.get("icon_name"),
                os.path.isfile("icons/{}.png".format(pkginfo["name"])),
                pkginfo.get("installer_type") == "apple_update_metadata",
        )):
            print("{}: missing icon".format(filename))
            retval = 1

        # Ensure uninstall method is set correctly if uninstall_script exists.
        if "uninstall_script" in pkginfo:
            if pkginfo.get("uninstall_method") != "uninstall_script":
                print(
                    '{}: has uninstall script, but the uninstall method is set to "{}"'
                    .format(filename, pkginfo.get("uninstall_method")))
                retval = 1

        # Ensure the items_to_copy list does not include trailing slashes.
        # Credit to @bruienne for this idea.
        # https://gist.github.com/bruienne/9baa958ec6dbe8f09d94#file-munki_fuzzinator-py-L211-L219
        if "items_to_copy" in pkginfo:
            for item_to_copy in pkginfo.get("items_to_copy"):
                if item_to_copy.get("destination_path").endswith("/"):
                    print(
                        '{}: has an items_to_copy with a trailing slash: "{}"'.
                        format(filename, item_to_copy["destination_path"]))
                    retval = 1

    return retval
Exemplo n.º 6
0
def main(argv=None):
    """Main process."""

    # Parse command line arguments.
    argparser = build_argument_parser()
    args = argparser.parse_args(argv)
    if args.strict:
        args.ignore_min_vers_before = "0.1.0"

    retval = 0
    for filename in args.filenames:
        try:
            recipe = plistlib.readPlist(filename)

        except (ExpatError, ValueError) as err:
            print("{}: plist parsing error: {}".format(filename, err))
            retval = 1
            break  # No need to continue checking this file

        # Top level keys that all AutoPkg recipes should contain.
        required_keys = ["Identifier"]
        if not validate_required_keys(recipe, filename, required_keys):
            retval = 1
            break  # No need to continue checking this file

        # Validate identifiers.
        if args.override_prefix and "Process" not in recipe:
            if not validate_override_prefix(recipe, filename,
                                            args.override_prefix):
                retval = 1
        if args.recipe_prefix and "Process" in recipe:
            if not validate_recipe_prefix(recipe, filename,
                                          args.recipe_prefix):
                retval = 1
        if recipe["Identifier"] == recipe.get("ParentRecipe"):
            print("{}: Identifier and ParentRecipe should not "
                  "be the same.".format(filename))
            retval = 1

        input_key = recipe.get("Input", recipe.get("input",
                                                   recipe.get("INPUT")))
        if input_key and "pkginfo" in input_key:
            if not validate_pkginfo_key_types(input_key["pkginfo"], filename):
                retval = 1

            # TODO: Additional pkginfo checks here.

        # Warn about comments that would be lost during `plutil -convert xml1`
        if not validate_comments(filename, args.strict):
            retval = 1

        # Processor checks.
        if "Process" in recipe:
            process = recipe["Process"]

            if not validate_processor_keys(process, filename):
                retval = 1

            if not validate_endofcheckphase(process, filename):
                retval = 1

            if not validate_no_var_in_app_path(process, filename):
                retval = 1

            min_vers = recipe.get("MinimumVersion")
            if min_vers and not validate_minimumversion(
                    process, min_vers, args.ignore_min_vers_before, filename):
                retval = 1

            if args.strict:
                if not validate_proc_type_conventions(process, filename):
                    retval = 1

                if not validate_required_proc_for_types(process, filename):
                    retval = 1

    return retval
def main(argv=None):
    """Main process."""

    # Parse command line arguments.
    argparser = build_argument_parser()
    args = argparser.parse_args(argv)
    if args.strict:
        args.ignore_min_vers_before = "0.1.0"

    retval = 0
    for filename in args.filenames:
        try:
            recipe = plistlib.readPlist(filename)

        except (ExpatError, ValueError) as err:
            print("{}: plist parsing error: {}".format(filename, err))
            retval = 1
            break  # No need to continue checking this file

        # Top level keys that all AutoPkg recipes should contain.
        required_keys = ["Identifier"]
        if not validate_required_keys(recipe, filename, required_keys):
            retval = 1
            break  # No need to continue checking this file

        # Warn if the recipe/override identifier does not start with the expected prefix.
        if args.override_prefix and "Process" not in recipe:
            override_prefix = args.override_prefix
            if not recipe["Identifier"].startswith(override_prefix):
                print(
                    '{}: override identifier does not start with "{}"'.format(
                        filename, override_prefix))
                retval = 1
        if args.recipe_prefix and "Process" in recipe:
            recipe_prefix = args.recipe_prefix
            if not recipe["Identifier"].startswith(recipe_prefix):
                print('{}: recipe identifier does not start with "{}"'.format(
                    filename, recipe_prefix))
                retval = 1

        input_key = recipe.get("Input", recipe.get("input",
                                                   recipe.get("INPUT")))
        if input_key and "pkginfo" in input_key:
            if not validate_pkginfo_key_types(input_key["pkginfo"], filename):
                retval = 1

            # TODO: Additional pkginfo checks here.

        # Warn about comments that would be lost during `plutil -convert xml1`
        with open(filename, "r") as openfile:
            recipe_text = openfile.read()
            if "<!--" in recipe_text and "-->" in recipe_text:
                if args.strict:
                    print("{}: Convert from <!-- --> style comments "
                          "to a Comment key.".format(filename))
                    retval = 1
                else:
                    print(
                        "{}: WARNING: Recommend converting from <!-- --> style comments "
                        "to a Comment key.".format(filename))

        # Processor checks.
        if "Process" in recipe:
            process = recipe["Process"]

            if not validate_processor_keys(process, filename):
                retval = 1

            if not validate_endofcheckphase(process, filename):
                retval = 1

            if not validate_no_var_in_app_path(process, filename):
                retval = 1

            min_vers = recipe.get("MinimumVersion")
            if min_vers and not validate_minimumversion(
                    process, min_vers, args.ignore_min_vers_before, filename):
                retval = 1

            if args.strict:
                if not validate_proc_type_conventions(process, filename):
                    retval = 1

                if not validate_required_proc_for_types(process, filename):
                    retval = 1

    return retval