Merge branch 'dev_2.0' into dev_2.0_generate_mapping

This commit is contained in:
Bob Gendler
2026-01-30 15:42:56 -05:00
9 changed files with 86 additions and 34 deletions

View File

@@ -86,8 +86,13 @@ Deploy a configuration profile containing the following payload.
|===
|ID
|{{ rule.rule_id }}
{{ rule.severity if rule.severity is not none and rule.tags not in check_tags }}
|{{ rule.rule_id }}
{% if rule.severity is not none %}
|{% trans %}Severity{% endtrans %}
|{{ rule.severity }}
{% endif %}
|{% trans %}References{% endtrans %}
|

View File

@@ -322,7 +322,7 @@ run_scan(){
{% for profile in baseline.profile %}
{% for rule in profile.rules %}
{% if "manual" not in rule.tags %}
{% if "manual" not in rule.tags and "Excluded" not in rule.section %}
{% include "check.jinja" %}
{% endif %}
{% endfor %}
@@ -363,7 +363,9 @@ run_fix(){
{% for profile in baseline.profile %}
{% for rule in profile.rules %}
{% include "fix.jinja" %}
{% if "manual" not in rule.tags and "Excluded" not in rule.section %}
{% include "fix.jinja" %}
{% endif %}
{% endfor %}
{% endfor %}

View File

@@ -175,7 +175,9 @@ run_default_state(){
{% for profile in baseline.profile %}
{% for rule in profile.rules %}
{% include "restore.jinja" %}
{% if "manual" not in rule.tags and "Excluded" not in rule.section %}
{% include "restore.jinja" %}
{% endif %}
{% endfor %}
{% endfor %}

View File

@@ -298,6 +298,23 @@
"max": 90
},
"note": "Delay the availability of a software update in days, e.g. 30"
},
{
"ruleId": "os_tv_content_allowed",
"type": "number",
"validation": {
"min": 0,
"max": 1000
},
"note": "Possible values, with the U.S. description of the rating level: `1000`: All | `600`: TV-MA | `500`: TV-14 | `400`: TV-PG | `300`: TV-G | `200`: TV-Y7 | `100`: TV-Y | `0`: None"
},
{
"ruleId": "os_movie_content_allowed",
"type": "number",
"validation": {
"min": 0,
"max": 1000
},
"note": "Possible values, with the U.S. description of the rating level: `1000`: All | `500`: NC-17 | `400`: R | `300`: PG-13 | `200`: PG | `100`: G | `0`: None"
}
]

View File

@@ -28,6 +28,7 @@ from ..common_utils.logger_instance import logger
_SENTINEL = object()
class Sectionmap(StrEnum):
AUDIT = "auditing"
AUTH = "authentication"
@@ -157,7 +158,6 @@ class References(BaseModelWithAccessors):
bsi: bsiReferences | None = None
custom_refs: customReferences | None = None
def get_ref(
self,
key: str,
@@ -221,8 +221,9 @@ class References(BaseModelWithAccessors):
# Not found
if default is not _SENTINEL:
return default
raise KeyError(f"Field '{key}' not found in any namespace ({', '.join(search_order)})")
raise KeyError(
f"Field '{key}' not found in any namespace ({', '.join(search_order)})"
)
class Macsecurityrule(BaseModelWithAccessors):
@@ -234,7 +235,6 @@ class Macsecurityrule(BaseModelWithAccessors):
Attributes:
title (str): The title of the security rule.
rule_id (str): Unique identifier for the rule.
severity (str): Severity level of the rule.
discussion (str): Detailed discussion or rationale for the rule.
references (References): Reference information (e.g., NIST, CIS) associated with the rule.
odv (dict[str, Any] | None): Organizational Defined Values for the rule, if applicable.
@@ -254,7 +254,7 @@ class Macsecurityrule(BaseModelWithAccessors):
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: (dict[str, Any]): The category for impact assigned to a rule for associated benchmarks.
severity: (str): The category for impact assigned to a rule for associated benchmarks.
default_state: (str): The command to restore the system to the default configuration for a rule.
Class Methods:
@@ -299,7 +299,7 @@ class Macsecurityrule(BaseModelWithAccessors):
os_version: float = Field(default_factory=float)
check: str | None = None
fix: str | None = None
severity: dict[str, Any] | None = None
severity: str | None = None
default_state: str | None = None
@classmethod
@@ -360,7 +360,7 @@ class Macsecurityrule(BaseModelWithAccessors):
default_state_value: str | None = None
mechanism: str = "Manual"
payloads: list[Mobileconfigpayload] | None = []
severity: dict[str, Any] | None = {}
severity: str | None = None
tags: list[str] = []
rule_file = next(
@@ -501,8 +501,8 @@ class Macsecurityrule(BaseModelWithAccessors):
if benchmarks:
for benchmark in benchmarks:
name = benchmark.get("name")
if "severity" in benchmark:
severity[name] = benchmark["severity"]
if "severity" in benchmark and name == parent_values:
severity = benchmark.get("severity", "")
match tags:
case "inherent":
@@ -536,6 +536,11 @@ class Macsecurityrule(BaseModelWithAccessors):
cis: dict[str, Any] = rule_yaml["references"].get("cis", {})
elif ref_key == "bsi":
bsi: dict[str, Any] = rule_yaml["references"].get("bsi", {})
elif ref_key == "custom": # support for 1.0 custom refs format
for custom_ref_key in rule_yaml["references"]["custom"]:
custom_refs[custom_ref_key] = rule_yaml["references"][
"custom"
].get(custom_ref_key, {})
else:
custom_refs[ref_key] = rule_yaml["references"].get(ref_key, {})
@@ -588,16 +593,8 @@ class Macsecurityrule(BaseModelWithAccessors):
bsi["indigo"] = [bsi["indigo"]]
# Map custom references
if custom_refs:
if "custom_refs" in custom_refs and isinstance(
custom_refs["custom_refs"], dict
):
if custom_refs["custom_refs"] is not None and not isinstance(
custom_refs["custom_refs"], list
):
rule_yaml["references"]["custom_refs"] = {}
rule_yaml["references"]["custom_refs"]["references"] = [
custom_refs
]
rule_yaml["references"]["custom_refs"] = {}
rule_yaml["references"]["custom_refs"]["references"] = [custom_refs]
rule = cls(
**rule_yaml,

View File

@@ -245,7 +245,7 @@ class Payload(BaseModel):
settings_dict (dict[str, Any]): The settings to save.
"""
try:
create_file(preferences_file, settings_dict)
create_file(preferences_file, settings_dict, append=True)
logger.success(f"Settings plist written to {preferences_file}")
except Exception as e:
logger.error(f"Error creating plist file {preferences_file}: {e}")

View File

@@ -404,10 +404,6 @@ def parse_cli() -> None:
)
sys.exit()
if args.os_name == "visionos":
logger.warning("visionOS is not supported at this time.")
sys.exit()
if args.subcommand == "guidance":
if args.os_name != "macos" and args.script:
logger.error(

View File

@@ -46,6 +46,31 @@ def collect_tags_and_benchmarks(
return sorted(tags_set), benchmark_platforms
def collect_established_benchmarks(
rules: list[Macsecurityrule],
) -> list[str]:
"""
Attempts to collect all established benchmarks in the MSCP library. An established
benchmark is one where an ODV has been defined for a given benchmark.
Args:
rules (list[Macsecurityrule]): A list of collected rules from the library.
Returns:
list: A sorted set of discovered benchmarks
"""
established_benchmarks_set: set[str] = set()
for rule in rules:
for odv in rule.odv or []:
established_benchmarks_set.add(odv)
# remove "hint" from available benchmarks
established_benchmarks_set.remove("hint")
return sorted(established_benchmarks_set)
def print_keyword_summary(
tags: list[str], benchmark_platforms: dict[str, set[str]]
) -> None:
@@ -97,7 +122,7 @@ def generate_baseline(args: argparse.Namespace) -> None:
baselines_data: dict = open_file(
Path(config.get("includes_dir", ""), "800-53_baselines.yaml")
)
established_benchmarks: tuple[str, ...] = ("stig", "cis_lvl1", "cis_lvl2")
# removing misc_tags, unsure we need it.
# misc_tags: tuple[str, str, str, str] = (
# "permanent",
@@ -148,6 +173,8 @@ def generate_baseline(args: argparse.Namespace) -> None:
all_tags, benchmark_map = collect_tags_and_benchmarks(all_rules)
established_benchmarks: tuple[str, ...] = collect_established_benchmarks(all_rules)
if args.list_tags:
print_keyword_summary(all_tags, benchmark_map)

View File

@@ -130,7 +130,9 @@ def generate_profiles(
signed_output_path: Path = Path(build_path, "mobileconfigs", "signed")
plist_output_path: Path = Path(build_path, "mobileconfigs", "preferences")
granular_output_path: Path = Path(build_path, "mobileconfigs", "granular")
granular_signed_output_path: Path = Path(build_path, "mobileconfigs", "granular", "signed")
granular_signed_output_path: Path = Path(
build_path, "mobileconfigs", "granular", "signed"
)
make_dir(unsigned_output_path)
make_dir(plist_output_path)
@@ -144,7 +146,10 @@ def generate_profiles(
make_dir(signed_output_path)
valid_rules: list[Macsecurityrule] = [
rule for profile in baseline.profile for rule in profile.rules
rule
for profile in baseline.profile
for rule in profile.rules
if "Excluded" not in rule.section
]
grouped_payloads: dict = get_payload_content_by_type(valid_rules)
@@ -218,7 +223,8 @@ def generate_profiles(
if signing:
sign_config_profile(
granular_output_path / f"{setting}.mobileconfig",
granular_signed_output_path / f"{setting}.mobileconfig",
granular_signed_output_path
/ f"{setting}.mobileconfig",
hash_value,
)
else: