From e4a1f933009ee9c1b942613851a81e18fd403514 Mon Sep 17 00:00:00 2001 From: James Smith Date: Wed, 19 Nov 2025 11:58:25 +1100 Subject: [PATCH 1/2] fix: Formats `import` statements, removes trailing whitespace & f-strings --- scripts/generate_guidance.py | 104 +++++++++++++++++++++++------------ 1 file changed, 68 insertions(+), 36 deletions(-) diff --git a/scripts/generate_guidance.py b/scripts/generate_guidance.py index c71072383..e7d0ab1b7 100755 --- a/scripts/generate_guidance.py +++ b/scripts/generate_guidance.py @@ -1,27 +1,28 @@ #!/usr/bin/env python3 # filename: generate_guidance.py # description: Process a given baseline, and output guidance files -import sys -import plistlib +import argparse +import base64 import glob +import hashlib +import json +import logging import os -import yaml +import plistlib import re -import argparse +import shutil import subprocess -import logging +import sys import tempfile -import base64 -import shutil -import json -import hashlib from datetime import date -from xlwt import Workbook, easyxf -from string import Template from itertools import groupby +from string import Template from uuid import uuid4 from zipfile import ZipFile +import yaml +from xlwt import Workbook, easyxf + class MacSecurityRule: def __init__( @@ -177,11 +178,11 @@ def format_mobileconfig_fix(mobileconfig): for k, v in item[1].items(): if type(v) == dict: rulefix = rulefix + (f" {k}\n") - rulefix = rulefix + (f" \n") + rulefix = rulefix + (" \n") for x, y in v.items(): rulefix = rulefix + (f" {x}\n") rulefix = rulefix + (f" {y}\n") - rulefix = rulefix + (f" \n") + rulefix = rulefix + (" \n") break if isinstance(v, list): rulefix = rulefix + " \n" @@ -402,7 +403,14 @@ def concatenate_payload_settings(settings): def generate_profiles( - baseline_name, build_path, parent_dir, baseline_yaml, signing, hash="", generate_domain=True, generate_consolidated=True + baseline_name, + build_path, + parent_dir, + baseline_yaml, + signing, + hash="", + generate_domain=True, + generate_consolidated=True, ): """Generate the configuration profiles for the rules in the provided baseline YAML file""" @@ -531,7 +539,7 @@ def generate_profiles( uuid=False, organization="macOS Security Compliance Project", displayname=f"{baseline_name} settings", - description=f"Consolidated configuration settings for {baseline_name}." + description=f"Consolidated configuration settings for {baseline_name}.", ) # process the payloads from the yaml file and generate new config profile for each type @@ -588,8 +596,12 @@ def generate_profiles( or (payload == "com.apple.systempreferences") or (payload == "com.apple.SetupAssistant.managed") ): - newProfile.addNewPayload(payload, concatenate_payload_settings(settings), baseline_name) - consolidated_profile.addNewPayload(payload, concatenate_payload_settings(settings), baseline_name) + newProfile.addNewPayload( + payload, concatenate_payload_settings(settings), baseline_name + ) + consolidated_profile.addNewPayload( + payload, concatenate_payload_settings(settings), baseline_name + ) else: newProfile.addNewPayload(payload, settings, baseline_name) consolidated_profile.addNewPayload(payload, settings, baseline_name) @@ -597,22 +609,36 @@ def generate_profiles( if generate_domain: with open(settings_plist_file_path, "wb") as settings_plist_file: newProfile.finalizeAndSavePlist(settings_plist_file) - with open(unsigned_mobileconfig_file_path, "wb") as unsigned_mobileconfig_file: + with open( + unsigned_mobileconfig_file_path, "wb" + ) as unsigned_mobileconfig_file: newProfile.finalizeAndSave(unsigned_mobileconfig_file) if signing: - sign_config_profile(unsigned_mobileconfig_file_path, signed_mobileconfig_file_path, hash) + sign_config_profile( + unsigned_mobileconfig_file_path, signed_mobileconfig_file_path, hash + ) if generate_consolidated: - consolidated_mobileconfig_file_path = os.path.join(unsigned_mobileconfig_output_path, f"{baseline_name}.mobileconfig") - with open(consolidated_mobileconfig_file_path, "wb") as consolidated_mobileconfig_file: + consolidated_mobileconfig_file_path = os.path.join( + unsigned_mobileconfig_output_path, f"{baseline_name}.mobileconfig" + ) + with open( + consolidated_mobileconfig_file_path, "wb" + ) as consolidated_mobileconfig_file: consolidated_profile.finalizeAndSave(consolidated_mobileconfig_file) if signing: - signed_consolidated_mobileconfig_path = os.path.join(signed_mobileconfig_output_path, f"{baseline_name}.mobileconfig") - sign_config_profile(consolidated_mobileconfig_file_path, signed_consolidated_mobileconfig_path, hash) + signed_consolidated_mobileconfig_path = os.path.join( + signed_mobileconfig_output_path, f"{baseline_name}.mobileconfig" + ) + sign_config_profile( + consolidated_mobileconfig_file_path, + signed_consolidated_mobileconfig_path, + hash, + ) print( - f""" + """ CAUTION: These configuration profiles are intended for evaluation in a TEST environment. Certain configuration profiles (Smartcards), when applied could leave a system in a state where a user can no longer login with a password. @@ -782,9 +808,9 @@ def generate_ddm(baseline_name, build_path, parent_dir, baseline_yaml): ddm_key_value ) else: - ddm_dict.setdefault(ddm_rule["ddm_info"]["declarationtype"], {}).update( - {ddm_key: ddm_key_value} - ) + ddm_dict.setdefault( + ddm_rule["ddm_info"]["declarationtype"], {} + ).update({ddm_key: ddm_key_value}) for ddm_type in mscp_data_yaml["ddm"]["supported_types"]: if ddm_type not in ddm_dict.keys(): @@ -1080,9 +1106,9 @@ def generate_script(baseline_name, audit_name, build_path, baseline_yaml, refere compliant=0 non_compliant=0 exempt_count=0 - + rule_names=($(/usr/libexec/PlistBuddy -c "Print" $audit_plist | awk '/= Dict/ {{print $1}}')) - + for rule in ${{rule_names[@]}}; do finding=$(/usr/libexec/PlistBuddy -c "Print $rule:finding" $audit_plist) if [[ $finding == "false" ]];then @@ -1095,7 +1121,7 @@ def generate_script(baseline_name, audit_name, build_path, baseline_yaml, refere if [[ $is_exempt == "1" ]]; then exempt_count=$((exempt_count+1)) non_compliant=$((non_compliant+1)) - else + else non_compliant=$((non_compliant+1)) fi fi @@ -1307,7 +1333,7 @@ def generate_script(baseline_name, audit_name, build_path, baseline_yaml, refere exempt_reason=$(/usr/bin/osascript -l JavaScript << EOS 2>/dev/null ObjC.unwrap($.NSUserDefaults.alloc.initWithSuiteName('org.{7}.audit').objectForKey('{0}'))["exempt_reason"] EOS -) +) customref="$(echo "{5}" | rev | cut -d ' ' -f 2- | rev)" customref="$(echo "$customref" | tr " " ",")" if [[ $result_value == "{4}" ]]; then @@ -1470,7 +1496,7 @@ def generate_script(baseline_name, audit_name, build_path, baseline_yaml, refere "--quiet= : 1 - show only failed and exempted checks in output" " 2 - show minimal output" ) - + # Look for managed arguments for compliance script if [[ $# -eq 0 ]];then compliance_args=$(/usr/bin/osascript -l JavaScript << 'EOS' @@ -1490,7 +1516,7 @@ def generate_script(baseline_name, audit_name, build_path, baseline_yaml, refere set -- ${{(z)compliance_args}} fi fi - + zparseopts -D -E -help=flag_help -check=check -fix=fix -stats=stats -compliant=compliant_opt -non_compliant=non_compliant_opt -reset=reset -reset-all=reset_all -cfc=cfc -quiet:=quiet || {{ print -l $usage && return }} [[ -z "$flag_help" ]] || {{ print -l $usage && return }} @@ -1663,7 +1689,7 @@ def get_rule_yaml( for yaml_field in og_rule_yaml: # print('processing field {} for rule {}'.format(yaml_field, file_name)) if yaml_field == "references": - if not "references" in resulting_yaml: + if "references" not in resulting_yaml: resulting_yaml["references"] = {} for ref in og_rule_yaml["references"]: try: @@ -2743,8 +2769,14 @@ def main(): # Single call to generate_profiles with both parameters generate_profiles( - baseline_name, build_path, parent_dir, baseline_yaml, signing, args.hash, - generate_domain=args.profiles, generate_consolidated=args.consolidated_profile + baseline_name, + build_path, + parent_dir, + baseline_yaml, + signing, + args.hash, + generate_domain=args.profiles, + generate_consolidated=args.consolidated_profile, ) if args.ddm: From 34ac0876d710faec36fbbe6b8370b3dc1cdec0a4 Mon Sep 17 00:00:00 2001 From: James Smith Date: Wed, 19 Nov 2025 12:10:14 +1100 Subject: [PATCH 2/2] feat: Adds CLI flags to `generate_guidance.py` script - Set `--identical_payload_identifier_uuid` with the `generate_guidance.py` script to use the same UUID for each `PayloadIdentifier`/`PayloadUUID` pair - Set `--no-creation-date` with the `generate_guidance.py` script to no longer include the created date in the `PayloadDescription` profile keys - Formats the import statements - Removes some whitespace throughout the `generate_guidance.py` script - Removes some f-strings that did not include any variables --- scripts/generate_guidance.py | 62 +++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/scripts/generate_guidance.py b/scripts/generate_guidance.py index e7d0ab1b7..0f85ccd84 100755 --- a/scripts/generate_guidance.py +++ b/scripts/generate_guidance.py @@ -214,7 +214,13 @@ class PayloadDict: """ def __init__( - self, identifier, uuid=False, description="", organization="", displayname="" + self, + identifier, + uuid=False, + description="", + organization="", + displayname="", + identical_payload_identifier_uuid=False, ): self.data = {} self.data["PayloadVersion"] = 1 @@ -227,7 +233,10 @@ def __init__( self.data["PayloadScope"] = "System" self.data["PayloadDescription"] = description self.data["PayloadDisplayName"] = displayname - self.data["PayloadIdentifier"] = identifier + if identical_payload_identifier_uuid: + self.data["PayloadIdentifier"] = self.data["PayloadUUID"] + else: + self.data["PayloadIdentifier"] = identifier self.data["ConsentText"] = { "default": "THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE. IN NO EVENT SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM, OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY, CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER." } @@ -235,6 +244,9 @@ def __init__( # An empty list for 'sub payloads' that we'll fill later self.data["PayloadContent"] = [] + # Store the flag for use later + self.identical_payload_identifier_uuid = identical_payload_identifier_uuid + def _updatePayload(self, payload_content_dict, baseline_name): """Update the profile with the payload settings. Takes the settings dictionary which will be the PayloadContent dict within the payload. Handles the boilerplate, naming and descriptive @@ -267,9 +279,12 @@ def _addPayload(self, payload_content_dict, baseline_name): payload_dict["PayloadVersion"] = 1 payload_dict["PayloadUUID"] = makeNewUUID() payload_dict["PayloadType"] = payload_content_dict["PayloadType"] - payload_dict["PayloadIdentifier"] = ( - f"mscp.{payload_content_dict['PayloadType']}.{payload_dict['PayloadUUID']}" - ) + if self.identical_payload_identifier_uuid: + payload_dict["PayloadIdentifier"] = payload_dict["PayloadUUID"] + else: + payload_dict["PayloadIdentifier"] = ( + f"mscp.{payload_content_dict['PayloadType']}.{payload_dict['PayloadUUID']}" + ) payload_dict["PayloadContent"] = payload_content_dict # Add the payload to the profile @@ -289,9 +304,12 @@ def addNewPayload(self, payload_type, settings, baseline_name): payload_dict["PayloadVersion"] = 1 payload_dict["PayloadUUID"] = makeNewUUID() payload_dict["PayloadType"] = payload_type - payload_dict["PayloadIdentifier"] = ( - f"mscp.{payload_type}.{payload_dict['PayloadUUID']}" - ) + if self.identical_payload_identifier_uuid: + payload_dict["PayloadIdentifier"] = payload_dict["PayloadUUID"] + else: + payload_dict["PayloadIdentifier"] = ( + f"mscp.{payload_type}.{payload_dict['PayloadUUID']}" + ) # Add the settings to the payload for setting in settings: @@ -409,6 +427,8 @@ def generate_profiles( baseline_yaml, signing, hash="", + no_created_date=False, + identical_payload_identifier_uuid=False, generate_domain=True, generate_consolidated=True, ): @@ -569,12 +589,15 @@ def generate_profiles( signed_mobileconfig_output_path, payload + ".mobileconfig" ) identifier = payload + f".{baseline_name}" - created = date.today() - description = ( - "Created: {}\nConfiguration settings for the {} preference domain.".format( + if no_created_date: + description = "Configuration settings for the {} preference domain.".format( + payload + ) + else: + created = date.today() + description = "Created: {}\nConfiguration settings for the {} preference domain.".format( created, payload ) - ) organization = "macOS Security Compliance Project" displayname = f"[{baseline_name}] {payload} settings" @@ -585,6 +608,7 @@ def generate_profiles( organization=organization, displayname=displayname, description=description, + identical_payload_identifier_uuid=identical_payload_identifier_uuid, ) if payload == "com.apple.ManagedClient.preferences": for item in settings: @@ -2131,6 +2155,18 @@ def create_args(): default=None, help="name of audit plist and log - defaults to baseline name", ) + parser.add_argument( + "--no-created-date", + default=False, + help="Do not add the created date to the profile description", + action="store_true", + ) + parser.add_argument( + "--identical-payload-identifier-uuid", + default=False, + help="Use the same UUID for each PayloadIdentifier and PayloadUUID pair within Configuration Profiles", + action="store_true", + ) return parser.parse_args() @@ -2775,6 +2811,8 @@ def main(): baseline_yaml, signing, args.hash, + args.no_created_date, + args.identical_payload_identifier_uuid, generate_domain=args.profiles, generate_consolidated=args.consolidated_profile, )