mirror of
https://github.com/usnistgov/macos_security.git
synced 2026-02-03 14:03:24 +00:00
refactor: update tailoring interaction
This commit is contained in:
@@ -21,6 +21,7 @@ from ..common_utils import (
|
||||
mscp_data,
|
||||
open_file,
|
||||
sanitize_input,
|
||||
prompt_for_odv,
|
||||
collect_overrides,
|
||||
)
|
||||
from ..common_utils.logger_instance import logger
|
||||
@@ -385,9 +386,11 @@ class Macsecurityrule(BaseModelWithAccessors):
|
||||
mechanism = "Configuration Profile"
|
||||
|
||||
for entry in rule_yaml["mobileconfig_info"]:
|
||||
payload_type: str = str(entry.get("PayloadType", ""))
|
||||
payload_type: str = str(
|
||||
entry.get("PayloadType", entry.get("payload_type", ""))
|
||||
)
|
||||
payload_content: list[dict[str, Any]] = entry.get(
|
||||
"PayloadContent", []
|
||||
"PayloadContent", entry.get("payload_content", [])
|
||||
)
|
||||
|
||||
payloads.append(
|
||||
@@ -504,9 +507,16 @@ class Macsecurityrule(BaseModelWithAccessors):
|
||||
bsi["indigo"] = [bsi["indigo"]]
|
||||
# Map custom references
|
||||
if custom_refs:
|
||||
# custom_refs["references"] = custom_refs
|
||||
rule_yaml["references"]["custom_refs"] = {}
|
||||
rule_yaml["references"]["custom_refs"]["references"] = [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 = cls(
|
||||
**rule_yaml,
|
||||
@@ -899,7 +909,10 @@ class Macsecurityrule(BaseModelWithAccessors):
|
||||
return
|
||||
|
||||
odv_lookup: dict[str, Any] = self.odv
|
||||
odv_value: str | int | bool | None = odv_lookup.get(parent_values)
|
||||
if "odv" in self.customized:
|
||||
odv_value: str | int | bool | None = odv_lookup.get("custom")
|
||||
else:
|
||||
odv_value: str | int | bool | None = odv_lookup.get(parent_values)
|
||||
if odv_value is None:
|
||||
return
|
||||
# Replace $ODV in text fields
|
||||
@@ -952,12 +965,14 @@ class Macsecurityrule(BaseModelWithAccessors):
|
||||
None
|
||||
"""
|
||||
rule_file_path: Path = Path(
|
||||
config["custom"]["rules"][self.section], f"{self.rule_id}.yaml"
|
||||
f"{config['custom']['rules_dir']}", f"{self.rule_id}.yaml"
|
||||
)
|
||||
|
||||
make_dir(rule_file_path.parent)
|
||||
|
||||
self["odv"] = {"custom": odv}
|
||||
self["odv"]["custom"] = odv
|
||||
|
||||
self.customized = []
|
||||
|
||||
self.to_yaml(rule_file_path)
|
||||
|
||||
@@ -975,7 +990,7 @@ class Macsecurityrule(BaseModelWithAccessors):
|
||||
None
|
||||
"""
|
||||
rule_file_path: Path = Path(
|
||||
config["custom"]["rules"][self.section], f"{self.rule_id}.yaml"
|
||||
f"{config['custom']['rules_dir']}", f"{self.rule_id}.yaml"
|
||||
)
|
||||
|
||||
if not rule_file_path.exists():
|
||||
@@ -983,8 +998,10 @@ class Macsecurityrule(BaseModelWithAccessors):
|
||||
return
|
||||
|
||||
if self.odv is not None and "custom" in self.odv:
|
||||
self.odv.pop("custom")
|
||||
self["references"].pop("custom")
|
||||
self.odv.pop("custom", 0)
|
||||
|
||||
# if "references" in self.customized:
|
||||
# self.references.pop("custom_refs")
|
||||
|
||||
self.to_yaml(rule_file_path)
|
||||
|
||||
@@ -1036,7 +1053,7 @@ class Macsecurityrule(BaseModelWithAccessors):
|
||||
include = sanitize_input(
|
||||
f'Would you like to include the rule for "{rule.rule_id}" in your benchmark? [Y/n/all/?]: ',
|
||||
str,
|
||||
range_=("y", "n", "all", "?"),
|
||||
range_=("Y", "y", "n", "all", "?"),
|
||||
default_="y",
|
||||
)
|
||||
if include == "?":
|
||||
@@ -1044,7 +1061,7 @@ class Macsecurityrule(BaseModelWithAccessors):
|
||||
include = sanitize_input(
|
||||
f'Would you like to include the rule for "{rule.rule_id}" in your benchmark? [Y/n/all]: ',
|
||||
str,
|
||||
range_=("y", "n", "all"),
|
||||
range_=("Y", "y", "n", "all"),
|
||||
default_="y",
|
||||
)
|
||||
queried_rule_ids.append(rule.rule_id)
|
||||
@@ -1062,22 +1079,21 @@ class Macsecurityrule(BaseModelWithAccessors):
|
||||
odv_hint = rule.odv.get("hint", "")
|
||||
odv_recommended = rule.odv.get("recommended")
|
||||
odv_benchmark = rule.odv.get(benchmark)
|
||||
|
||||
if benchmark == "recommended":
|
||||
print(f"{odv_hint}")
|
||||
odv = sanitize_input(
|
||||
print(f"\nODV value: {odv_hint['description']}")
|
||||
odv = prompt_for_odv(
|
||||
f'Enter the ODV for "{rule.rule_id}" or press Enter for the recommended value ({odv_recommended}): ',
|
||||
type(odv_recommended),
|
||||
default_=odv_recommended,
|
||||
odv_hint=odv_hint,
|
||||
default=odv_recommended,
|
||||
)
|
||||
if odv != odv_recommended:
|
||||
rule.write_odv_custom_rule(odv)
|
||||
else:
|
||||
print(f"\nODV value: {odv_hint}")
|
||||
odv = sanitize_input(
|
||||
print(f"\nODV value: {odv_hint['description']}")
|
||||
odv = prompt_for_odv(
|
||||
f'Enter the ODV for "{rule.rule_id}" or press Enter for the default value ({odv_benchmark}): ',
|
||||
type(odv_benchmark),
|
||||
default_=odv_benchmark,
|
||||
odv_hint=odv_hint,
|
||||
default=odv_benchmark,
|
||||
)
|
||||
if odv != odv_benchmark:
|
||||
rule.write_odv_custom_rule(odv)
|
||||
@@ -1107,7 +1123,7 @@ class Macsecurityrule(BaseModelWithAccessors):
|
||||
"platforms",
|
||||
)
|
||||
|
||||
rule_file_path: Path = output_path / f"{self.rule_id}.yaml"
|
||||
rule_file_path: Path = output_path
|
||||
serialized_data: dict[str, Any] = self.model_dump()
|
||||
ordered_data = OrderedDict()
|
||||
|
||||
@@ -1115,10 +1131,10 @@ class Macsecurityrule(BaseModelWithAccessors):
|
||||
|
||||
serialized_data["references"]["nist"]["800-53r5"] = serialized_data[
|
||||
"references"
|
||||
].pop("nist_800_53r5")
|
||||
].pop("nist_800_53r5", 0)
|
||||
serialized_data["references"]["nist"]["800-171r3"] = serialized_data[
|
||||
"references"
|
||||
].pop("nist_800_171")
|
||||
].pop("nist_800_171", 0)
|
||||
|
||||
for key in serialized_data["references"]:
|
||||
if isinstance(serialized_data["references"][key], list):
|
||||
|
||||
@@ -27,6 +27,7 @@ from .logging_config import set_logger
|
||||
from .mscp_data import get_mscp_data, mscp_data
|
||||
from .run_command import run_command
|
||||
from .sanitize_input import sanitize_input
|
||||
from .prompt_for_odv import prompt_for_odv
|
||||
from .validate_rules import validate_yaml_file
|
||||
from .version_data import get_version_data
|
||||
|
||||
@@ -49,6 +50,7 @@ __all__ = [
|
||||
"remove_file",
|
||||
"run_command",
|
||||
"sanitize_input",
|
||||
"prompt_for_odv",
|
||||
"get_version_data",
|
||||
"mscp_data",
|
||||
"get_mscp_data",
|
||||
|
||||
163
src/mscp/common_utils/prompt_for_odv.py
Normal file
163
src/mscp/common_utils/prompt_for_odv.py
Normal file
@@ -0,0 +1,163 @@
|
||||
# mscp/common_utils/prompt_for_odv.py
|
||||
|
||||
# Standard python modules
|
||||
from typing import Any, Dict, Optional
|
||||
import re
|
||||
|
||||
# Local python modules
|
||||
from .logger_instance import logger
|
||||
|
||||
|
||||
import re
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
def prompt_for_odv(
|
||||
prompt: str,
|
||||
odv_hint: Dict[str, Any],
|
||||
default: Optional[Any] = None,
|
||||
) -> Any:
|
||||
"""
|
||||
Prompt the user for an 'organization defined value' (ODV) using a single hint dict
|
||||
that defines datatype, description, and validation rules. Reprompts until valid.
|
||||
|
||||
Args:
|
||||
prompt (str): The prompt shown to the user.
|
||||
odv_hint (dict): Hint metadata containing:
|
||||
- 'datatype' (str): one of 'number', 'string', 'enum', 'regex'
|
||||
- 'description' (str): guidance shown next to the prompt
|
||||
- 'validation' (dict): rule set keyed by datatype:
|
||||
* number: {'min': <int>, 'max': <int>} (both optional)
|
||||
* regex: {'regex': <str>} (required for datatype='regex')
|
||||
* enum: {'enumValues': [<str>, ...]} (required for datatype='enum')
|
||||
* string: {'regex': <str>} (optional; applied to raw input)
|
||||
default (Any, optional): Value used when user hits Enter; will be validated.
|
||||
|
||||
Returns:
|
||||
Any: The validated value (coerced according to the datatype).
|
||||
|
||||
Behavior:
|
||||
- Regex rules apply to the raw text first (if present).
|
||||
- Values are then coerced according to 'datatype'.
|
||||
- Range/membership rules apply after coercion.
|
||||
- If default is provided, it is validated before being returned.
|
||||
"""
|
||||
|
||||
# ---- helpers ------------------------------------------------------------
|
||||
|
||||
def _apply_regex(text: str, pattern: str) -> Optional[str]:
|
||||
"""Return error message if text does NOT match the pattern; else None."""
|
||||
try:
|
||||
if re.fullmatch(pattern, text) is None:
|
||||
return f"Value must match the pattern: {pattern}"
|
||||
return None
|
||||
except re.error as ex:
|
||||
return f"Invalid regex pattern in hint: {ex}"
|
||||
|
||||
def _coerce(text: str, dt: str) -> Any:
|
||||
"""Coerce raw text to the target datatype."""
|
||||
# default to string if no datatype is provided
|
||||
dt_norm = (dt or "string").strip().lower()
|
||||
if dt_norm == "number":
|
||||
return int(text) # will raise ValueError if invalid
|
||||
elif dt_norm in ("string", "enum", "regex"):
|
||||
return text # keep as string; enum/regex validated separately
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported datatype '{dt}'. Use 'number', 'string', 'enum', or 'regex'."
|
||||
)
|
||||
|
||||
def _validate(
|
||||
value: Any, raw_text: str, dt: str, rules: Dict[str, Any]
|
||||
) -> Optional[str]:
|
||||
"""Return error message string if invalid; else None."""
|
||||
|
||||
# if rules are not defined, accept any values
|
||||
if not rules:
|
||||
return None
|
||||
|
||||
dt_norm = (dt or "").strip().lower()
|
||||
|
||||
# 1) Regex on raw input if present (for both string and regex modes)
|
||||
regex = rules.get("regex")
|
||||
if regex:
|
||||
err = _apply_regex(raw_text, regex)
|
||||
if err:
|
||||
return err
|
||||
|
||||
# 2) Datatype-specific checks after coercion
|
||||
if dt_norm == "number":
|
||||
min_v = rules.get("min", None)
|
||||
max_v = rules.get("max", None)
|
||||
if min_v is not None and value < min_v:
|
||||
return f"Value must be ≥ {min_v}."
|
||||
if max_v is not None and value > max_v:
|
||||
return f"Value must be ≤ {max_v}."
|
||||
|
||||
elif dt_norm == "enum":
|
||||
opts = rules.get("enumValues")
|
||||
if not opts or not isinstance(opts, (list, tuple, set)):
|
||||
return "Missing 'enumValues' for datatype='enum'."
|
||||
# case-insensitive membership convenience
|
||||
lowered = {str(o).lower() for o in opts}
|
||||
if str(value).lower() not in lowered:
|
||||
return f"Value must be one of: {list(opts)}."
|
||||
|
||||
elif dt_norm == "regex":
|
||||
# Already checked via regex above; enforce presence of rule
|
||||
if "regex" not in rules:
|
||||
return "Missing 'regex' rule for datatype='regex'."
|
||||
|
||||
# string: nothing additional beyond optional regex
|
||||
return None
|
||||
|
||||
# ---- main loop ----------------------------------------------------------
|
||||
|
||||
dt = (odv_hint.get("datatype") or "").strip().lower()
|
||||
desc = odv_hint.get("description", "")
|
||||
rules = odv_hint.get("validation", {}) or {}
|
||||
|
||||
# Build a friendly prompt line
|
||||
# hint = f" — {desc}" if desc else ""
|
||||
# default_hint = f" [default: {default}]" if default not in (None, "") else ""
|
||||
# display = f"{prompt}{hint}{default_hint}"
|
||||
|
||||
while True:
|
||||
raw = input(f"{prompt}").strip()
|
||||
|
||||
# Handle default
|
||||
if raw == "":
|
||||
if default is not None:
|
||||
# Validate default too
|
||||
try:
|
||||
coerced_default = (
|
||||
_coerce(str(default), dt) if dt == "number" else default
|
||||
)
|
||||
except ValueError as ex:
|
||||
print(f"Default value cannot be coerced: {ex}")
|
||||
# continue reprompting
|
||||
continue
|
||||
err = _validate(coerced_default, str(default), dt, rules)
|
||||
if err is None:
|
||||
return coerced_default
|
||||
else:
|
||||
print(f"Default value invalid: {err}")
|
||||
continue
|
||||
else:
|
||||
print("Please enter a value (or provide a default).")
|
||||
continue
|
||||
|
||||
# Coerce user entry
|
||||
try:
|
||||
value = _coerce(raw, dt)
|
||||
except ValueError as ex:
|
||||
print(f"\nERROR - Value must be a valid integer")
|
||||
continue
|
||||
|
||||
# Validate combined rules
|
||||
error = _validate(value, raw_text=raw, dt=dt, rules=rules)
|
||||
if error is None:
|
||||
return value
|
||||
|
||||
print(f"\nERROR - {error}")
|
||||
# loop continues
|
||||
Reference in New Issue
Block a user