def main(argv=None): """Main process.""" # Top level keys that all AutoPkg recipes should contain. required_keys = ("Identifier", "Input") # Parse command line arguments. argparser = build_argument_parser() args = argparser.parse_args(argv) retval = 0 for filename in args.filenames: try: recipe = plistlib.readPlist(filename) for req_key in required_keys: if req_key not in recipe: print("{}: missing required key {}".format( filename, req_key)) retval = 1 break # No need to continue checking this file except (ExpatError, ValueError) as err: print("{}: plist parsing error: {}".format(filename, err)) retval = 1 if args.override_prefix and "Process" not in recipe: override_prefix = args.override_prefix if not recipe.get("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.get("Identifier", "").startswith(recipe_prefix): print('{}: recipe identifier does not start with "{}"'.format( filename, recipe_prefix)) retval = 1 input = recipe.get("Input", recipe.get("input", recipe.get("INPUT"))) if input and "pkginfo" in input: retval = validate_pkginfo_key_types(input["pkginfo"], 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: 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
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
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