feat: add consolidated and granular profile generation

options are now available to include either a single consolidated profile
or individual profiles for each setting
This commit is contained in:
Dan Brodjieski
2026-01-05 14:12:33 -05:00
parent 10f62f2a4e
commit 7aad3a592f
5 changed files with 111 additions and 8 deletions

View File

@@ -184,6 +184,11 @@ class Macsecurityrule(BaseModelWithAccessors):
platforms (dict[str, Platforms]): Platform-specific data for the rule.
os_name (str): Name of the operating system.
os_type (str): Type of the operating system.
os_version: float = Field(default_factory=float)
check (str): The commands to evaluate the state of a rule.
fix: (str): The commands to remediate and set the configuration for a rule.
severity: (str): The category for impact assigned to a rule.
default_state: (str): The command to restore the system to the default configuration for a rule.
Class Methods:
load_rules: Load Macsecurityrule objects from YAML files for the given rule IDs.

View File

@@ -90,7 +90,7 @@ class Payload(BaseModel):
"PayloadVersion": self.payload_version,
"PayloadUUID": uuid,
"PayloadType": payload_type,
"PayloadIdentifier": f"alacarte.macOS.{baseline_name}.{uuid}",
"PayloadIdentifier": f"mscp.{payload_type}.{uuid}",
}
payload.update(settings)
@@ -121,7 +121,7 @@ class Payload(BaseModel):
"PayloadVersion": self.payload_version,
"PayloadUUID": uuid,
"PayloadType": "com.apple.ManagedClient.preferences",
"PayloadIdentifier": f"alacarte.macOS.{baseline_name}.{uuid}",
"PayloadIdentifier": f"mscp.{domain}.{uuid}",
"PayloadContent": {
domain: {"Forced": [{"mcx_preference_settings": settings}]}
},

View File

@@ -166,6 +166,20 @@ def parse_cli() -> None:
help="Generate configuration profiles for the rules.",
action="store_true",
)
guidance_parser.add_argument(
"-P",
"--consolidated-profile",
default=False,
help="Include a single consolidated configuration profile when generating profiles.",
action="store_true",
)
guidance_parser.add_argument(
"-G",
"--granular-profiles",
default=False,
help="Include granular per-setting configuration profiles when generating profiles.",
action="store_true",
)
guidance_parser.add_argument(
"-r",
"--reference",
@@ -383,6 +397,10 @@ def parse_cli() -> None:
logger.warning("visionOS is not supported at this time.")
sys.exit()
# if generating consolidated profile, assume to do all profiles
if args.consolidated_profile or args.granular_profiles:
args.profiles = True
if args.subcommand == "guidance":
if args.os_name != "macos" and args.script:
logger.error(

View File

@@ -129,9 +129,23 @@ def generate_guidance(args: argparse.Namespace) -> None:
if args.profiles:
logger.info("Generating configuration profiles")
if not signing:
generate_profiles(build_path, baseline_name, baseline)
generate_profiles(
build_path,
baseline_name,
baseline,
consolidated=args.consolidated_profile,
granular=args.granular_profiles,
)
else:
generate_profiles(build_path, baseline_name, baseline, signing, args.hash)
generate_profiles(
build_path,
baseline_name,
baseline,
signing,
args.hash,
consolidated=args.consolidated_profile,
granular=args.granular_profiles,
)
if args.ddm:
logger.info("Generating declarative components")
@@ -170,7 +184,13 @@ def generate_guidance(args: argparse.Namespace) -> None:
if args.all:
logger.info("Generating all support files")
logger.info("Generating configuration profiles")
generate_profiles(build_path, baseline_name, baseline)
generate_profiles(
build_path,
baseline_name,
baseline,
consolidated=args.consolidated_profile,
granular=args.granular_profiles,
)
logger.info("Generating declarative components")
generate_ddm(build_path, baseline, baseline_name)

View File

@@ -78,6 +78,8 @@ def generate_profiles(
baseline: Baseline,
signing: bool = False,
hash_value: str = "",
consolidated: bool = False,
granular: bool = False,
) -> None:
"""
Generates configuration profiles based on the provided baseline and saves them to the specified build path.
@@ -88,6 +90,8 @@ def generate_profiles(
baseline (Baseline): The baseline object containing profile and rule information.
signing (bool, optional): Whether to sign the generated profiles. Defaults to False.
hash_value (str, optional): The hash value used for signing the profiles. Defaults to an empty string.
consolidated (bool, optional): Whether to include a single consolidate profile containing all the settings.
granular (bool, optional): Whether to build individual profiles for every setting.
Returns:
None
@@ -108,7 +112,7 @@ def generate_profiles(
unsigned_output_path: Path = Path(build_path, "mobileconfigs", "unsigned")
signed_output_path: Path = Path(build_path, "mobileconfigs", "signed")
plist_output_path: Path = Path(build_path, "mobileconfigs", "preferences")
create_date: date = date.today()
granular_output_path: Path = Path(build_path, "mobileconfigs", "granular")
manifests_file: dict = open_file(
Path(config.get("includes_dir", ""), "supported_payloads.yaml")
@@ -116,6 +120,7 @@ def generate_profiles(
make_dir(unsigned_output_path)
make_dir(plist_output_path)
make_dir(granular_output_path)
if signing:
make_dir(signed_output_path)
@@ -149,6 +154,13 @@ def generate_profiles(
for error in profile_errors:
logger.info(f"Correct the following rule: {error.rule_id}")
consolidated_profile = Payload(
identifier=f"consolidated.{baseline_name}",
organization="macOS Security Compliance Project",
displayname=f"{baseline_name} settings",
description=f"Consolidated configuration settings for {baseline_name}.",
)
for payload_type, settings_list in grouped_payloads.items():
logger.debug("Payload Type: {}", payload_type)
logger.debug("Settings List: {}", repr(settings_list))
@@ -171,9 +183,8 @@ def generate_profiles(
sanitized_payload_type = "".join(
c if c.isalnum() or c in "._-" else "_" for c in payload_type
)
identifier = f"{sanitized_payload_type}.{baseline_name}"
identifier = f"mscp.{sanitized_payload_type}.{baseline_name}"
description = (
f"Created: {create_date}\n"
f"Configuration settings for the {payload_type} preference domain."
)
organization = "macOS Security Compliance Project"
@@ -190,9 +201,45 @@ def generate_profiles(
for settings in flat_settings:
for domain, payload_content in settings.items():
new_profile.add_mcx_payload(domain, payload_content, baseline_name)
consolidated_profile.add_mcx_payload(
domain, payload_content, baseline_name
)
# generate individual profiles for each setting
if granular:
for setting, value in payload_content.items():
granular_profile = Payload(
identifier=f"mscp.{domain}.{setting}",
organization=organization,
description=f"Configuration for {domain}:{setting}",
displayname=f"[{domain}] - {setting}",
)
granular_profile.add_mcx_payload(
domain, {setting: value}, setting
)
granular_profile.save_to_plist(
granular_output_path / f"{setting}.mobileconfig"
)
else:
settings: dict = {k: v for d in flat_settings for k, v in d.items()}
new_profile.add_payload(payload_type, settings, baseline_name)
consolidated_profile.add_payload(payload_type, settings, baseline_name)
# generate individual profiles for each setting
if granular:
for setting, value in settings.items():
granular_profile = Payload(
identifier=f"mscp.{payload_type}.{setting}",
organization=organization,
description=f"Configuration for {payload_type}:{setting}",
displayname=f"[{payload_type}] - {setting}",
)
granular_profile.add_payload(
payload_type, {setting: value}, setting
)
granular_profile.save_to_plist(
granular_output_path / f"{setting}.mobileconfig"
)
new_profile.save_to_plist(unsigned_mobileconfig_file_path)
@@ -209,6 +256,19 @@ def generate_profiles(
f"Configuration profile for {payload_type} saved to {unsigned_mobileconfig_file_path}"
)
# write consolidated profile if enabled
if consolidated:
consolidated_profile.save_to_plist(
unsigned_output_path / f"{baseline_name}.mobileconfig"
)
if signing:
sign_config_profile(
unsigned_output_path / f"{baseline_name}.mobileconfig",
signed_output_path / f"{baseline_name}.mobileconfig",
hash_value,
)
managed_client_file: Path = (
plist_output_path / "com.apple.ManagedClient.preferences.plist"
)